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:
-rw-r--r--.eslintrc1
-rw-r--r--.gitlab-ci.yml289
-rw-r--r--.gitlab/merge_request_templates/Database Changes.md73
-rw-r--r--.rubocop.yml80
-rw-r--r--.rubocop_todo.yml26
-rw-r--r--CHANGELOG.md292
-rw-r--r--CONTRIBUTING.md16
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile47
-rw-r--r--Gemfile.lock175
-rw-r--r--PROCESS.md63
-rw-r--r--README.md1
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/api.js42
-rw-r--r--app/assets/javascripts/autosave.js24
-rw-r--r--app/assets/javascripts/awards_handler.js30
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js5
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js30
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js51
-rw-r--r--app/assets/javascripts/boards/components/board_list.js10
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js5
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js11
-rw-r--r--app/assets/javascripts/boards/components/modal/footer.js2
-rw-r--r--app/assets/javascripts/boards/components/modal/list.js2
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js28
-rw-r--r--app/assets/javascripts/boards/models/issue.js4
-rw-r--r--app/assets/javascripts/boards/models/label.js1
-rw-r--r--app/assets/javascripts/boards/models/list.js28
-rw-r--r--app/assets/javascripts/boards/services/board_service.js20
-rw-r--r--app/assets/javascripts/breadcrumb.js28
-rw-r--r--app/assets/javascripts/breakpoints.js77
-rw-r--r--app/assets/javascripts/build.js7
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js1
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue5
-rw-r--r--app/assets/javascripts/commits.js2
-rw-r--r--app/assets/javascripts/commons/index.js1
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/commons/polyfills/nodelist.js7
-rw-r--r--app/assets/javascripts/commons/vue.js5
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js14
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js8
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js8
-rw-r--r--app/assets/javascripts/dispatcher.js39
-rw-r--r--app/assets/javascripts/droplab/drop_down.js7
-rw-r--r--app/assets/javascripts/dropzone_input.js4
-rw-r--r--app/assets/javascripts/due_date_select.js2
-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/filtered_search/dropdown_emoji.js82
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js1
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js5
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js14
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js20
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js21
-rw-r--r--app/assets/javascripts/fly_out_nav.js171
-rw-r--r--app/assets/javascripts/gl_dropdown.js26
-rw-r--r--app/assets/javascripts/gpg_badges.js4
-rw-r--r--app/assets/javascripts/group_name.js76
-rw-r--r--app/assets/javascripts/groups/components/group_identicon.vue45
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue6
-rw-r--r--app/assets/javascripts/groups_select.js3
-rw-r--r--app/assets/javascripts/helpers/user_feature_helper.js12
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js10
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js25
-rw-r--r--app/assets/javascripts/issuable_context.js3
-rw-r--r--app/assets/javascripts/issuable_form.js54
-rw-r--r--app/assets/javascripts/issue.js9
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue29
-rw-r--r--app/assets/javascripts/issue_show/components/edited.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue8
-rw-r--r--app/assets/javascripts/issue_show/components/fields/project_move.vue83
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue22
-rw-r--r--app/assets/javascripts/issue_show/index.js6
-rw-r--r--app/assets/javascripts/issue_show/stores/index.js1
-rw-r--r--app/assets/javascripts/jobs/components/header.vue2
-rw-r--r--app/assets/javascripts/layout_nav.js15
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js22
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js13
-rw-r--r--app/assets/javascripts/lib/utils/sticky.js4
-rw-r--r--app/assets/javascripts/main.js19
-rw-r--r--app/assets/javascripts/member_expiration_date.js4
-rw-r--r--app/assets/javascripts/merge_request_tabs.js24
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue145
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue96
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue275
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue133
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue88
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue179
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue21
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring.vue157
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_column.vue297
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_deployment.vue136
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_flag.vue104
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_legends.vue144
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_paths.vue40
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_row.vue41
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_state.vue112
-rw-r--r--app/assets/javascripts/monitoring/constants.js4
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js4
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js6
-rw-r--r--app/assets/javascripts/monitoring/stores/monitoring_store.js70
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js15
-rw-r--r--app/assets/javascripts/monitoring/utils/measurements.js12
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js80
-rw-r--r--app/assets/javascripts/new_sidebar.js17
-rw-r--r--app/assets/javascripts/notes.js8
-rw-r--r--app/assets/javascripts/notes/components/issue_comment_form.vue347
-rw-r--r--app/assets/javascripts/notes/components/issue_discussion.vue232
-rw-r--r--app/assets/javascripts/notes/components/issue_note.vue186
-rw-r--r--app/assets/javascripts/notes/components/issue_note_actions.vue167
-rw-r--r--app/assets/javascripts/notes/components/issue_note_attachment.vue37
-rw-r--r--app/assets/javascripts/notes/components/issue_note_awards_list.vue228
-rw-r--r--app/assets/javascripts/notes/components/issue_note_body.vue122
-rw-r--r--app/assets/javascripts/notes/components/issue_note_edited_text.vue47
-rw-r--r--app/assets/javascripts/notes/components/issue_note_form.vue166
-rw-r--r--app/assets/javascripts/notes/components/issue_note_header.vue118
-rw-r--r--app/assets/javascripts/notes/components/issue_note_icons.js37
-rw-r--r--app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue28
-rw-r--r--app/assets/javascripts/notes/components/issue_notes_app.vue151
-rw-r--r--app/assets/javascripts/notes/components/issue_placeholder_note.vue53
-rw-r--r--app/assets/javascripts/notes/components/issue_placeholder_system_note.vue21
-rw-r--r--app/assets/javascripts/notes/components/issue_system_note.vue55
-rw-r--r--app/assets/javascripts/notes/constants.js11
-rw-r--r--app/assets/javascripts/notes/event_hub.js3
-rw-r--r--app/assets/javascripts/notes/index.js35
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js16
-rw-r--r--app/assets/javascripts/notes/services/issue_notes_service.js35
-rw-r--r--app/assets/javascripts/notes/stores/actions.js217
-rw-r--r--app/assets/javascripts/notes/stores/getters.js31
-rw-r--r--app/assets/javascripts/notes/stores/index.js23
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js14
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js151
-rw-r--r--app/assets/javascripts/notes/stores/utils.js31
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js50
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue59
-rw-r--r--app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue23
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/navigation_tabs.vue54
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue63
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue9
-rw-r--r--app/assets/javascripts/project.js8
-rw-r--r--app/assets/javascripts/project_select.js42
-rw-r--r--app/assets/javascripts/project_select_combo_button.js49
-rw-r--r--app/assets/javascripts/project_visibility.js41
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_feature_setting.vue104
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue51
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_setting_row.vue36
-rw-r--r--app/assets/javascripts/projects/permissions/components/settings_panel.vue312
-rw-r--r--app/assets/javascripts/projects/permissions/constants.js11
-rw-r--r--app/assets/javascripts/projects/permissions/external.js18
-rw-r--r--app/assets/javascripts/projects/permissions/index.js13
-rw-r--r--app/assets/javascripts/projects/project_new.js2
-rw-r--r--app/assets/javascripts/projects_dropdown/components/app.vue157
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue57
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_item.vue96
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_search.vue63
-rw-r--r--app/assets/javascripts/projects_dropdown/components/search.vue64
-rw-r--r--app/assets/javascripts/projects_dropdown/constants.js10
-rw-r--r--app/assets/javascripts/projects_dropdown/event_hub.js3
-rw-r--r--app/assets/javascripts/projects_dropdown/index.js68
-rw-r--r--app/assets/javascripts/projects_dropdown/service/projects_service.js132
-rw-r--r--app/assets/javascripts/projects_dropdown/store/projects_store.js33
-rw-r--r--app/assets/javascripts/render_gfm.js4
-rw-r--r--app/assets/javascripts/repo/components/repo.vue55
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue94
-rw-r--r--app/assets/javascripts/repo/components/repo_edit_button.vue39
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue104
-rw-r--r--app/assets/javascripts/repo/components/repo_file.vue67
-rw-r--r--app/assets/javascripts/repo/components/repo_file_buttons.vue47
-rw-r--r--app/assets/javascripts/repo/components/repo_file_options.vue2
-rw-r--r--app/assets/javascripts/repo/components/repo_loading_file.vue61
-rw-r--r--app/assets/javascripts/repo/components/repo_prev_directory.vue16
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue30
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue59
-rw-r--r--app/assets/javascripts/repo/components/repo_tab.vue38
-rw-r--r--app/assets/javascripts/repo/components/repo_tabs.vue25
-rw-r--r--app/assets/javascripts/repo/helpers/monaco_loader_helper.js8
-rw-r--r--app/assets/javascripts/repo/helpers/repo_helper.js92
-rw-r--r--app/assets/javascripts/repo/index.js8
-rw-r--r--app/assets/javascripts/repo/monaco_loader.js6
-rw-r--r--app/assets/javascripts/repo/services/repo_service.js28
-rw-r--r--app/assets/javascripts/repo/stores/repo_store.js68
-rw-r--r--app/assets/javascripts/right_sidebar.js11
-rw-r--r--app/assets/javascripts/search_autocomplete.js2
-rw-r--r--app/assets/javascripts/settings_panels.js4
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js15
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.js2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js4
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js85
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js20
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js7
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js29
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js10
-rw-r--r--app/assets/javascripts/sidebar_height_manager.js34
-rw-r--r--app/assets/javascripts/user_callout.js12
-rw-r--r--app/assets/javascripts/users_select.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js10
-rw-r--r--app/assets/javascripts/vue_shared/common_vue.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/identicon.vue51
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue97
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue93
-rw-r--r--app/assets/javascripts/vue_shared/components/popup_dialog.vue64
-rw-r--r--app/assets/javascripts/vue_shared/directives/popover.js20
-rw-r--r--app/assets/javascripts/webpack.js2
-rw-r--r--app/assets/javascripts/wikis.js11
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/animations.scss78
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/badges.scss2
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/buttons.scss19
-rw-r--r--app/assets/stylesheets/framework/calendar.scss10
-rw-r--r--app/assets/stylesheets/framework/common.scss25
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss281
-rw-r--r--app/assets/stylesheets/framework/feature_highlight.scss94
-rw-r--r--app/assets/stylesheets/framework/filters.scss28
-rw-r--r--app/assets/stylesheets/framework/flash.scss2
-rw-r--r--app/assets/stylesheets/framework/forms.scss6
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss265
-rw-r--r--app/assets/stylesheets/framework/header.scss22
-rw-r--r--app/assets/stylesheets/framework/highlight.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss4
-rw-r--r--app/assets/stylesheets/framework/lists.scss6
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss10
-rw-r--r--app/assets/stylesheets/framework/mixins.scss2
-rw-r--r--app/assets/stylesheets/framework/modal.scss10
-rw-r--r--app/assets/stylesheets/framework/nav.scss29
-rw-r--r--app/assets/stylesheets/framework/page-header.scss2
-rw-r--r--app/assets/stylesheets/framework/selects.scss62
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/snippets.scss2
-rw-r--r--app/assets/stylesheets/framework/tables.scss2
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss4
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss24
-rw-r--r--app/assets/stylesheets/framework/variables.scss59
-rw-r--r--app/assets/stylesheets/framework/wells.scss2
-rw-r--r--app/assets/stylesheets/highlight/dark.scss8
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss2
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss2
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss2
-rw-r--r--app/assets/stylesheets/highlight/white.scss32
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss32
-rw-r--r--app/assets/stylesheets/new_nav.scss443
-rw-r--r--app/assets/stylesheets/new_sidebar.scss129
-rw-r--r--app/assets/stylesheets/pages/boards.scss66
-rw-r--r--app/assets/stylesheets/pages/builds.scss15
-rw-r--r--app/assets/stylesheets/pages/ci_projects.scss2
-rw-r--r--app/assets/stylesheets/pages/commits.scss27
-rw-r--r--app/assets/stylesheets/pages/convdev_index.scss6
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss10
-rw-r--r--app/assets/stylesheets/pages/diff.scss26
-rw-r--r--app/assets/stylesheets/pages/editor.scss2
-rw-r--r--app/assets/stylesheets/pages/environments.scss46
-rw-r--r--app/assets/stylesheets/pages/events.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss46
-rw-r--r--app/assets/stylesheets/pages/issues.scss27
-rw-r--r--app/assets/stylesheets/pages/labels.scss2
-rw-r--r--app/assets/stylesheets/pages/login.scss8
-rw-r--r--app/assets/stylesheets/pages/members.scss14
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss40
-rw-r--r--app/assets/stylesheets/pages/milestone.scss4
-rw-r--r--app/assets/stylesheets/pages/note_form.scss32
-rw-r--r--app/assets/stylesheets/pages/notes.scss126
-rw-r--r--app/assets/stylesheets/pages/notifications.scss4
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss243
-rw-r--r--app/assets/stylesheets/pages/profile.scss4
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss64
-rw-r--r--app/assets/stylesheets/pages/projects.scss253
-rw-r--r--app/assets/stylesheets/pages/repo.scss182
-rw-r--r--app/assets/stylesheets/pages/runners.scss2
-rw-r--r--app/assets/stylesheets/pages/search.scss6
-rw-r--r--app/assets/stylesheets/pages/settings.scss41
-rw-r--r--app/assets/stylesheets/pages/sherlock.scss2
-rw-r--r--app/assets/stylesheets/pages/todos.scss10
-rw-r--r--app/assets/stylesheets/pages/tree.scss9
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss2
-rw-r--r--app/assets/stylesheets/pages/wiki.scss4
-rw-r--r--app/assets/stylesheets/pages/xterm.scss2
-rw-r--r--app/assets/stylesheets/print.scss2
-rw-r--r--app/controllers/admin/appearances_controller.rb2
-rw-r--r--app/controllers/admin/application_settings_controller.rb3
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb2
-rw-r--r--app/controllers/admin/dashboard_controller.rb6
-rw-r--r--app/controllers/admin/logs_controller.rb16
-rw-r--r--app/controllers/admin/projects_controller.rb6
-rw-r--r--app/controllers/admin/users_controller.rb18
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/autocomplete_controller.rb24
-rw-r--r--app/controllers/boards/application_controller.rb21
-rw-r--r--app/controllers/boards/issues_controller.rb90
-rw-r--r--app/controllers/boards/lists_controller.rb75
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb2
-rw-r--r--app/controllers/concerns/boards_responses.rb42
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb9
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/issuable_collections.rb40
-rw-r--r--app/controllers/concerns/notes_actions.rb56
-rw-r--r--app/controllers/concerns/renders_commits.rb7
-rw-r--r--app/controllers/concerns/renders_notes.rb9
-rw-r--r--app/controllers/concerns/requires_whitelisted_monitoring_client.rb3
-rw-r--r--app/controllers/dashboard/groups_controller.rb4
-rw-r--r--app/controllers/dashboard/projects_controller.rb8
-rw-r--r--app/controllers/dashboard_controller.rb6
-rw-r--r--app/controllers/explore/projects_controller.rb11
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/groups/milestones_controller.rb6
-rw-r--r--app/controllers/groups_controller.rb21
-rw-r--r--app/controllers/import/github_controller.rb8
-rw-r--r--app/controllers/import/gitlab_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb12
-rw-r--r--app/controllers/passwords_controller.rb10
-rw-r--r--app/controllers/profiles/emails_controller.rb2
-rw-r--r--app/controllers/profiles/keys_controller.rb2
-rw-r--r--app/controllers/profiles/passwords_controller.rb2
-rw-r--r--app/controllers/profiles/preferences_controller.rb3
-rw-r--r--app/controllers/profiles_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/artifacts_controller.rb12
-rw-r--r--app/controllers/projects/blob_controller.rb5
-rw-r--r--app/controllers/projects/boards/application_controller.rb15
-rw-r--r--app/controllers/projects/boards/issues_controller.rb94
-rw-r--r--app/controllers/projects/boards/lists_controller.rb86
-rw-r--r--app/controllers/projects/boards_controller.rb27
-rw-r--r--app/controllers/projects/commit_controller.rb2
-rw-r--r--app/controllers/projects/commits_controller.rb3
-rw-r--r--app/controllers/projects/compare_controller.rb3
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb24
-rw-r--r--app/controllers/projects/issues_controller.rb75
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests_controller.rb22
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb12
-rw-r--r--app/controllers/projects/project_members_controller.rb4
-rw-r--r--app/controllers/projects/services_controller.rb9
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb5
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb20
-rw-r--r--app/controllers/search_controller.rb7
-rw-r--r--app/controllers/sessions_controller.rb8
-rw-r--r--app/controllers/snippets_controller.rb2
-rw-r--r--app/controllers/unicorn_test_controller.rb4
-rw-r--r--app/controllers/uploads_controller.rb2
-rw-r--r--app/finders/admin/projects_finder.rb82
-rw-r--r--app/finders/groups_finder.rb36
-rw-r--r--app/finders/issuable_finder.rb43
-rw-r--r--app/finders/issues_finder.rb39
-rw-r--r--app/finders/merge_requests_finder.rb1
-rw-r--r--app/finders/move_to_project_finder.rb1
-rw-r--r--app/finders/projects_finder.rb19
-rw-r--r--app/finders/todos_finder.rb2
-rw-r--r--app/helpers/appearances_helper.rb2
-rw-r--r--app/helpers/application_helper.rb8
-rw-r--r--app/helpers/application_settings_helper.rb22
-rw-r--r--app/helpers/auth_helper.rb2
-rw-r--r--app/helpers/auto_devops_helper.rb7
-rw-r--r--app/helpers/avatars_helper.rb11
-rw-r--r--app/helpers/blame_helper.rb16
-rw-r--r--app/helpers/boards_helper.rb77
-rw-r--r--app/helpers/breadcrumbs_helper.rb12
-rw-r--r--app/helpers/button_helper.rb20
-rw-r--r--app/helpers/commits_helper.rb8
-rw-r--r--app/helpers/dropdowns_helper.rb6
-rw-r--r--app/helpers/events_helper.rb11
-rw-r--r--app/helpers/form_helper.rb4
-rw-r--r--app/helpers/graph_helper.rb2
-rw-r--r--app/helpers/groups_helper.rb89
-rw-r--r--app/helpers/import_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb76
-rw-r--r--app/helpers/issues_helper.rb31
-rw-r--r--app/helpers/labels_helper.rb11
-rw-r--r--app/helpers/markup_helper.rb30
-rw-r--r--app/helpers/milestones_helper.rb15
-rw-r--r--app/helpers/milestones_routing_helper.rb8
-rw-r--r--app/helpers/namespaces_helper.rb39
-rw-r--r--app/helpers/nav_helper.rb23
-rw-r--r--app/helpers/notes_helper.rb14
-rw-r--r--app/helpers/page_layout_helper.rb6
-rw-r--r--app/helpers/pagination_helper.rb21
-rw-r--r--app/helpers/pipeline_schedules_helper.rb10
-rw-r--r--app/helpers/preferences_helper.rb4
-rw-r--r--app/helpers/profiles_helper.rb13
-rw-r--r--app/helpers/projects_helper.rb79
-rw-r--r--app/helpers/search_helper.rb29
-rw-r--r--app/helpers/system_note_helper.rb8
-rw-r--r--app/helpers/tab_helper.rb4
-rw-r--r--app/helpers/tree_helper.rb4
-rw-r--r--app/helpers/version_check_helper.rb2
-rw-r--r--app/helpers/visibility_level_helper.rb77
-rw-r--r--app/helpers/wiki_helper.rb11
-rw-r--r--app/mailers/base_mailer.rb4
-rw-r--r--app/mailers/emails/members.rb4
-rw-r--r--app/models/appearance.rb20
-rw-r--r--app/models/application_setting.rb29
-rw-r--r--app/models/award_emoji.rb7
-rw-r--r--app/models/blob_viewer/composer_json.rb2
-rw-r--r--app/models/blob_viewer/notebook.rb2
-rw-r--r--app/models/board.rb14
-rw-r--r--app/models/broadcast_message.rb38
-rw-r--r--app/models/ci/build.rb21
-rw-r--r--app/models/ci/pipeline.rb55
-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/runner.rb12
-rw-r--r--app/models/ci/stage.rb63
-rw-r--r--app/models/ci/trigger_request.rb4
-rw-r--r--app/models/commit.rb50
-rw-r--r--app/models/commit_status.rb22
-rw-r--r--app/models/concerns/awardable.rb15
-rw-r--r--app/models/concerns/cache_markdown_field.rb6
-rw-r--r--app/models/concerns/editable.rb2
-rw-r--r--app/models/concerns/has_status.rb2
-rw-r--r--app/models/concerns/internal_id.rb2
-rw-r--r--app/models/concerns/issuable.rb18
-rw-r--r--app/models/concerns/mentionable.rb4
-rw-r--r--app/models/concerns/milestoneish.rb8
-rw-r--r--app/models/concerns/noteable.rb6
-rw-r--r--app/models/concerns/participable.rb2
-rw-r--r--app/models/concerns/project_features_compatibility.rb2
-rw-r--r--app/models/concerns/protected_ref.rb2
-rw-r--r--app/models/concerns/referable.rb9
-rw-r--r--app/models/concerns/relative_positioning.rb14
-rw-r--r--app/models/concerns/resolvable_discussion.rb1
-rw-r--r--app/models/concerns/resolvable_note.rb28
-rw-r--r--app/models/concerns/sortable.rb4
-rw-r--r--app/models/concerns/spammable.rb8
-rw-r--r--app/models/concerns/storage/legacy_project.rb76
-rw-r--r--app/models/concerns/token_authenticatable.rb3
-rw-r--r--app/models/dashboard_milestone.rb2
-rw-r--r--app/models/deploy_keys_project.rb2
-rw-r--r--app/models/deployment.rb2
-rw-r--r--app/models/discussion.rb4
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/models/event.rb131
-rw-r--r--app/models/event_collection.rb98
-rw-r--r--app/models/gpg_key.rb23
-rw-r--r--app/models/gpg_signature.rb16
-rw-r--r--app/models/group.rb80
-rw-r--r--app/models/group_milestone.rb2
-rw-r--r--app/models/issue.rb20
-rw-r--r--app/models/key.rb31
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/member.rb88
-rw-r--r--app/models/members/group_member.rb4
-rw-r--r--app/models/members/project_member.rb4
-rw-r--r--app/models/merge_request.rb62
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/merge_request_diff_commit.rb2
-rw-r--r--app/models/milestone.rb6
-rw-r--r--app/models/namespace.rb32
-rw-r--r--app/models/network/commit.rb2
-rw-r--r--app/models/network/graph.rb6
-rw-r--r--app/models/note.rb77
-rw-r--r--app/models/notification_recipient.rb76
-rw-r--r--app/models/notification_setting.rb2
-rw-r--r--app/models/project.rb277
-rw-r--r--app/models/project_auto_devops.rb11
-rw-r--r--app/models/project_feature.rb4
-rw-r--r--app/models/project_services/chat_notification_service.rb8
-rw-r--r--app/models/project_services/hipchat_service.rb8
-rw-r--r--app/models/project_services/kubernetes_service.rb6
-rw-r--r--app/models/project_statistics.rb2
-rw-r--r--app/models/project_team.rb2
-rw-r--r--app/models/protectable_dropdown.rb8
-rw-r--r--app/models/protected_branch.rb2
-rw-r--r--app/models/push_event.rb81
-rw-r--r--app/models/push_event_payload.rb22
-rw-r--r--app/models/redirect_route.rb10
-rw-r--r--app/models/repository.rb185
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/models/storage/hashed_project.rb42
-rw-r--r--app/models/storage/legacy_project.rb51
-rw-r--r--app/models/user.rb89
-rw-r--r--app/models/user_synced_attributes_metadata.rb25
-rw-r--r--app/models/wiki_page.rb2
-rw-r--r--app/policies/base_policy.rb4
-rw-r--r--app/policies/group_policy.rb11
-rw-r--r--app/policies/project_policy.rb6
-rw-r--r--app/presenters/ci/build_presenter.rb11
-rw-r--r--app/serializers/analytics_build_entity.rb2
-rw-r--r--app/serializers/analytics_issue_entity.rb2
-rw-r--r--app/serializers/award_emoji_entity.rb4
-rw-r--r--app/serializers/discussion_entity.rb10
-rw-r--r--app/serializers/discussion_serializer.rb3
-rw-r--r--app/serializers/issuable_entity.rb2
-rw-r--r--app/serializers/issue_entity.rb20
-rw-r--r--app/serializers/job_entity.rb2
-rw-r--r--app/serializers/note_attachment_entity.rb5
-rw-r--r--app/serializers/note_entity.rb60
-rw-r--r--app/serializers/note_serializer.rb3
-rw-r--r--app/serializers/note_user_entity.rb3
-rw-r--r--app/serializers/pipeline_entity.rb1
-rw-r--r--app/serializers/project_entity.rb2
-rw-r--r--app/serializers/tree_root_entity.rb15
-rw-r--r--app/serializers/user_serializer.rb3
-rw-r--r--app/services/akismet_service.rb6
-rw-r--r--app/services/auth/container_registry_authentication_service.rb2
-rw-r--r--app/services/boards/base_service.rb10
-rw-r--r--app/services/boards/create_service.rb6
-rw-r--r--app/services/boards/issues/create_service.rb12
-rw-r--r--app/services/boards/issues/list_service.rb10
-rw-r--r--app/services/boards/issues/move_service.rb20
-rw-r--r--app/services/boards/list_service.rb8
-rw-r--r--app/services/boards/lists/create_service.rb9
-rw-r--r--app/services/boards/lists/destroy_service.rb2
-rw-r--r--app/services/boards/lists/generate_service.rb6
-rw-r--r--app/services/boards/lists/list_service.rb2
-rw-r--r--app/services/boards/lists/move_service.rb2
-rw-r--r--app/services/ci/create_pipeline_service.rb54
-rw-r--r--app/services/ci/create_trigger_request_service.rb19
-rw-r--r--app/services/ci/register_job_service.rb10
-rw-r--r--app/services/ci/retry_build_service.rb4
-rw-r--r--app/services/commits/change_service.rb1
-rw-r--r--app/services/commits/create_service.rb2
-rw-r--r--app/services/compare_service.rb22
-rw-r--r--app/services/concerns/update_visibility_level.rb15
-rw-r--r--app/services/concerns/users/new_user_notifier.rb9
-rw-r--r--app/services/create_branch_service.rb2
-rw-r--r--app/services/delete_branch_service.rb2
-rw-r--r--app/services/discussions/update_diff_position_service.rb4
-rw-r--r--app/services/event_create_service.rb9
-rw-r--r--app/services/git_hooks_service.rb32
-rw-r--r--app/services/git_operation_service.rb157
-rw-r--r--app/services/git_push_service.rb25
-rw-r--r--app/services/groups/create_service.rb38
-rw-r--r--app/services/groups/destroy_service.rb2
-rw-r--r--app/services/groups/nested_create_service.rb49
-rw-r--r--app/services/groups/update_service.rb27
-rw-r--r--app/services/issuable_base_service.rb22
-rw-r--r--app/services/issues/create_service.rb3
-rw-r--r--app/services/issues/update_service.rb29
-rw-r--r--app/services/labels/transfer_service.rb2
-rw-r--r--app/services/members/destroy_service.rb2
-rw-r--r--app/services/merge_requests/create_from_issue_service.rb5
-rw-r--r--app/services/merge_requests/create_service.rb4
-rw-r--r--app/services/merge_requests/merge_service.rb8
-rw-r--r--app/services/merge_requests/merge_when_pipeline_succeeds_service.rb2
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/services/milestones/close_service.rb2
-rw-r--r--app/services/milestones/create_service.rb2
-rw-r--r--app/services/milestones/destroy_service.rb2
-rw-r--r--app/services/milestones/reopen_service.rb2
-rw-r--r--app/services/notification_recipient_service.rb2
-rw-r--r--app/services/notification_service.rb53
-rw-r--r--app/services/projects/after_import_service.rb24
-rw-r--r--app/services/projects/count_service.rb43
-rw-r--r--app/services/projects/create_service.rb19
-rw-r--r--app/services/projects/destroy_service.rb2
-rw-r--r--app/services/projects/fork_service.rb6
-rw-r--r--app/services/projects/forks_count_service.rb12
-rw-r--r--app/services/projects/housekeeping_service.rb5
-rw-r--r--app/services/projects/open_issues_count_service.rb15
-rw-r--r--app/services/projects/open_merge_requests_count_service.rb13
-rw-r--r--app/services/projects/unlink_fork_service.rb6
-rw-r--r--app/services/projects/update_pages_service.rb8
-rw-r--r--app/services/projects/update_service.rb22
-rw-r--r--app/services/push_event_payload_service.rb120
-rw-r--r--app/services/quick_actions/interpret_service.rb20
-rw-r--r--app/services/spam_service.rb2
-rw-r--r--app/services/system_hooks_service.rb2
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/services/tags/create_service.rb2
-rw-r--r--app/services/tags/destroy_service.rb2
-rw-r--r--app/services/test_hooks/base_service.rb9
-rw-r--r--app/services/test_hooks/system_service.rb37
-rw-r--r--app/services/upload_service.rb2
-rw-r--r--app/services/user_project_access_changed_service.rb10
-rw-r--r--app/services/users/build_service.rb2
-rw-r--r--app/services/users/create_service.rb8
-rw-r--r--app/services/users/destroy_service.rb6
-rw-r--r--app/services/users/update_service.rb10
-rw-r--r--app/services/validate_new_branch_service.rb2
-rw-r--r--app/services/web_hook_service.rb2
-rw-r--r--app/uploaders/personal_file_uploader.rb2
-rw-r--r--app/validators/key_restriction_validator.rb29
-rw-r--r--app/views/admin/application_settings/_form.html.haml89
-rw-r--r--app/views/admin/applications/edit.html.haml2
-rw-r--r--app/views/admin/cohorts/_usage_ping.html.haml2
-rw-r--r--app/views/admin/cohorts/index.html.haml1
-rw-r--r--app/views/admin/dashboard/_head.html.haml4
-rw-r--r--app/views/admin/dashboard/index.html.haml1
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/admin/hooks/edit.html.haml1
-rw-r--r--app/views/admin/jobs/index.html.haml1
-rw-r--r--app/views/admin/labels/edit.html.haml2
-rw-r--r--app/views/admin/logs/show.html.haml16
-rw-r--r--app/views/admin/monitoring/_head.html.haml4
-rw-r--r--app/views/admin/projects/show.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml1
-rw-r--r--app/views/admin/services/edit.html.haml2
-rw-r--r--app/views/admin/users/_user.html.haml3
-rw-r--r--app/views/admin/users/show.html.haml2
-rw-r--r--app/views/ci/lints/_create.html.haml4
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml6
-rw-r--r--app/views/ci/variables/_content.html.haml12
-rw-r--r--app/views/ci/variables/_index.html.haml4
-rw-r--r--app/views/ci/variables/_show.html.haml2
-rw-r--r--app/views/dashboard/_groups_head.html.haml8
-rw-r--r--app/views/dashboard/_projects_head.html.haml8
-rw-r--r--app/views/dashboard/_snippets_head.html.haml6
-rw-r--r--app/views/dashboard/issues.html.haml10
-rw-r--r--app/views/dashboard/merge_requests.html.haml8
-rw-r--r--app/views/dashboard/milestones/index.html.haml8
-rw-r--r--app/views/dashboard/projects/_blank_state_admin_welcome.html.haml2
-rw-r--r--app/views/discussions/_headline.html.haml6
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml78
-rw-r--r--app/views/events/_commit.html.haml4
-rw-r--r--app/views/events/_event_push.atom.haml26
-rw-r--r--app/views/events/event/_push.html.haml11
-rw-r--r--app/views/feature_highlight/_issue_boards.svg98
-rw-r--r--app/views/groups/edit.html.haml18
-rw-r--r--app/views/groups/issues.html.haml10
-rw-r--r--app/views/groups/labels/index.html.haml5
-rw-r--r--app/views/groups/merge_requests.html.haml12
-rw-r--r--app/views/groups/milestones/index.html.haml5
-rw-r--r--app/views/groups/milestones/show.html.haml2
-rw-r--r--app/views/groups/projects.html.haml1
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml3
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/groups/subgroups.html.haml1
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml3
-rw-r--r--app/views/kaminari/gitlab/_without_count.html.haml8
-rw-r--r--app/views/layouts/_head.html.haml6
-rw-r--r--app/views/layouts/_page.html.haml26
-rw-r--r--app/views/layouts/_search.html.haml4
-rw-r--r--app/views/layouts/admin.html.haml7
-rw-r--r--app/views/layouts/application.html.haml7
-rw-r--r--app/views/layouts/errors.html.haml4
-rw-r--r--app/views/layouts/group.html.haml7
-rw-r--r--app/views/layouts/header/_default.html.haml79
-rw-r--r--app/views/layouts/header/_new.html.haml85
-rw-r--r--app/views/layouts/header/_new_dropdown.haml8
-rw-r--r--app/views/layouts/nav/_admin.html.haml40
-rw-r--r--app/views/layouts/nav/_admin_settings.html.haml31
-rw-r--r--app/views/layouts/nav/_breadcrumbs.html.haml31
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml123
-rw-r--r--app/views/layouts/nav/_explore.html.haml30
-rw-r--r--app/views/layouts/nav/_group.html.haml31
-rw-r--r--app/views/layouts/nav/_new_admin_sidebar.html.haml150
-rw-r--r--app/views/layouts/nav/_new_dashboard.html.haml33
-rw-r--r--app/views/layouts/nav/_new_explore.html.haml19
-rw-r--r--app/views/layouts/nav/_new_group_sidebar.html.haml89
-rw-r--r--app/views/layouts/nav/_new_profile_sidebar.html.haml84
-rw-r--r--app/views/layouts/nav/_new_project_sidebar.html.haml261
-rw-r--r--app/views/layouts/nav/_profile.html.haml57
-rw-r--r--app/views/layouts/nav/_project.html.haml109
-rw-r--r--app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml11
-rw-r--r--app/views/layouts/nav/projects_dropdown/_show.html.haml15
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml212
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml118
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml144
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml323
-rw-r--r--app/views/layouts/oauth_error.html.haml2
-rw-r--r--app/views/layouts/profile.html.haml7
-rw-r--r--app/views/layouts/project.html.haml15
-rw-r--r--app/views/profiles/accounts/show.html.haml37
-rw-r--r--app/views/profiles/gpg_keys/index.html.haml2
-rw-r--r--app/views/profiles/keys/_key.html.haml8
-rw-r--r--app/views/profiles/keys/_key_details.html.haml1
-rw-r--r--app/views/profiles/passwords/edit.html.haml1
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml1
-rw-r--r--app/views/profiles/preferences/show.html.haml64
-rw-r--r--app/views/profiles/preferences/update.js.erb4
-rw-r--r--app/views/profiles/show.html.haml20
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml5
-rw-r--r--app/views/projects/_activity.html.haml2
-rw-r--r--app/views/projects/_export.html.haml41
-rw-r--r--app/views/projects/_flash_messages.html.haml3
-rw-r--r--app/views/projects/_last_push.html.haml6
-rw-r--r--app/views/projects/_md_preview.html.haml7
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml6
-rw-r--r--app/views/projects/_project_templates.html.haml4
-rw-r--r--app/views/projects/activity.html.haml4
-rw-r--r--app/views/projects/artifacts/browse.html.haml4
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_upload.html.haml4
-rw-r--r--app/views/projects/boards/_show.html.haml41
-rw-r--r--app/views/projects/boards/components/_board.html.haml46
-rw-r--r--app/views/projects/boards/components/_sidebar.html.haml27
-rw-r--r--app/views/projects/boards/components/sidebar/_assignee.html.haml32
-rw-r--r--app/views/projects/boards/components/sidebar/_due_date.html.haml32
-rw-r--r--app/views/projects/boards/components/sidebar/_labels.html.haml30
-rw-r--r--app/views/projects/boards/components/sidebar/_milestone.html.haml29
-rw-r--r--app/views/projects/boards/components/sidebar/_notifications.html.haml7
-rw-r--r--app/views/projects/boards/index.html.haml2
-rw-r--r--app/views/projects/boards/show.html.haml2
-rw-r--r--app/views/projects/branches/_commit.html.haml2
-rw-r--r--app/views/projects/branches/index.html.haml3
-rw-r--r--app/views/projects/commit/_ajax_signature.html.haml1
-rw-r--r--app/views/projects/commit/_invalid_signature_badge.html.haml9
-rw-r--r--app/views/projects/commit/_other_user_signature_badge.html.haml6
-rw-r--r--app/views/projects/commit/_pipelines_list.haml1
-rw-r--r--app/views/projects/commit/_same_user_different_email_signature_badge.html.haml7
-rw-r--r--app/views/projects/commit/_signature.html.haml5
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml22
-rw-r--r--app/views/projects/commit/_signature_badge_user.html.haml21
-rw-r--r--app/views/projects/commit/_unknown_key_signature_badge.html.haml1
-rw-r--r--app/views/projects/commit/_unverified_key_signature_badge.html.haml1
-rw-r--r--app/views/projects/commit/_unverified_signature_badge.html.haml6
-rw-r--r--app/views/projects/commit/_valid_signature_badge.html.haml32
-rw-r--r--app/views/projects/commit/_verified_signature_badge.html.haml7
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml13
-rw-r--r--app/views/projects/commits/_inline_commit.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml3
-rw-r--r--app/views/projects/compare/index.html.haml3
-rw-r--r--app/views/projects/compare/show.html.haml4
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml5
-rw-r--r--app/views/projects/deployments/_commit.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml32
-rw-r--r--app/views/projects/diffs/_stats.html.haml6
-rw-r--r--app/views/projects/edit.html.haml124
-rw-r--r--app/views/projects/empty.html.haml1
-rw-r--r--app/views/projects/environments/index.html.haml4
-rw-r--r--app/views/projects/environments/show.html.haml2
-rw-r--r--app/views/projects/graphs/charts.html.haml2
-rw-r--r--app/views/projects/graphs/show.html.haml3
-rw-r--r--app/views/projects/issues/_discussion.html.haml14
-rw-r--r--app/views/projects/issues/_issues.html.haml2
-rw-r--r--app/views/projects/issues/_nav_btns.html.haml2
-rw-r--r--app/views/projects/issues/index.html.haml6
-rw-r--r--app/views/projects/issues/show.html.haml15
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml10
-rw-r--r--app/views/projects/jobs/index.html.haml3
-rw-r--r--app/views/projects/jobs/show.html.haml2
-rw-r--r--app/views/projects/labels/index.html.haml6
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml6
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/merge_requests/_nav_btns.html.haml2
-rw-r--r--app/views/projects/merge_requests/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/index.html.haml8
-rw-r--r--app/views/projects/merge_requests/show.html.haml2
-rw-r--r--app/views/projects/milestones/index.html.haml8
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml15
-rw-r--r--app/views/projects/notes/_actions.html.haml50
-rw-r--r--app/views/projects/notes/_more_actions_dropdown.html.haml11
-rw-r--r--app/views/projects/pipeline_schedules/edit.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml10
-rw-r--r--app/views/projects/pipeline_schedules/new.html.haml3
-rw-r--r--app/views/projects/pipelines/charts.html.haml3
-rw-r--r--app/views/projects/pipelines/charts/_pipelines.haml6
-rw-r--r--app/views/projects/pipelines/index.html.haml34
-rw-r--r--app/views/projects/pipelines/show.html.haml2
-rw-r--r--app/views/projects/pipelines_settings/_badge.html.haml62
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml61
-rw-r--r--app/views/projects/project_members/import.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml3
-rw-r--r--app/views/projects/releases/edit.html.haml2
-rw-r--r--app/views/projects/runners/_form.html.haml6
-rw-r--r--app/views/projects/runners/_runner.html.haml2
-rw-r--r--app/views/projects/runners/show.html.haml3
-rw-r--r--app/views/projects/services/edit.html.haml4
-rw-r--r--app/views/projects/settings/_head.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml58
-rw-r--r--app/views/projects/settings/integrations/show.html.haml3
-rw-r--r--app/views/projects/settings/repository/show.html.haml4
-rw-r--r--app/views/projects/show.html.haml10
-rw-r--r--app/views/projects/snippets/edit.html.haml2
-rw-r--r--app/views/projects/snippets/index.html.haml6
-rw-r--r--app/views/projects/snippets/new.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml4
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--app/views/projects/tree/_tree_commit_column.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml5
-rw-r--r--app/views/projects/tree/_tree_item.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml2
-rw-r--r--app/views/projects/triggers/_content.html.haml10
-rw-r--r--app/views/projects/triggers/_index.html.haml3
-rw-r--r--app/views/projects/wikis/pages.html.haml2
-rw-r--r--app/views/projects/wikis/show.html.haml7
-rw-r--r--app/views/search/_form.html.haml2
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml15
-rw-r--r--app/views/shared/_event_filter.html.haml12
-rw-r--r--app/views/shared/_group_form.html.haml5
-rw-r--r--app/views/shared/_import_form.html.haml2
-rw-r--r--app/views/shared/_label.html.haml4
-rw-r--r--app/views/shared/_logo.svg2
-rw-r--r--app/views/shared/_new_project_item_select.html.haml4
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/views/shared/_target_switcher.html.haml2
-rw-r--r--app/views/shared/_user_dropdown_experimental_features.html.haml1
-rw-r--r--app/views/shared/_visibility_level.html.haml2
-rw-r--r--app/views/shared/_visibility_radios.html.haml20
-rw-r--r--app/views/shared/boards/_show.html.haml40
-rw-r--r--app/views/shared/boards/components/_board.html.haml47
-rw-r--r--app/views/shared/boards/components/_sidebar.html.haml28
-rw-r--r--app/views/shared/boards/components/sidebar/_assignee.html.haml32
-rw-r--r--app/views/shared/boards/components/sidebar/_due_date.html.haml32
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml37
-rw-r--r--app/views/shared/boards/components/sidebar/_milestone.html.haml29
-rw-r--r--app/views/shared/boards/components/sidebar/_notifications.html.haml7
-rw-r--r--app/views/shared/boards/index.html.haml1
-rw-r--r--app/views/shared/boards/show.html.haml1
-rw-r--r--app/views/shared/empty_states/_issues.html.haml2
-rw-r--r--app/views/shared/empty_states/_merge_requests.html.haml2
-rw-r--r--app/views/shared/icons/_caret_down.svg1
-rw-r--r--app/views/shared/icons/_ellipsis_v.svg1
-rw-r--r--app/views/shared/icons/_express.svg (renamed from app/views/shared/icons/_node_express.svg)0
-rw-r--r--app/views/shared/icons/_icon_arrow_circle_o_right.svg3
-rw-r--r--app/views/shared/icons/_icon_arrow_right.svg.erb1
-rw-r--r--app/views/shared/icons/_icon_autodevops.svg54
-rw-r--r--app/views/shared/icons/_icon_check_square_o.svg3
-rw-r--r--app/views/shared/icons/_icon_clock_o.svg2
-rw-r--r--app/views/shared/icons/_icon_clone.svg5
-rw-r--r--app/views/shared/icons/_icon_code_fork.svg3
-rw-r--r--app/views/shared/icons/_icon_comment_o.svg3
-rw-r--r--app/views/shared/icons/_icon_commit.svg3
-rw-r--r--app/views/shared/icons/_icon_edit.svg3
-rw-r--r--app/views/shared/icons/_icon_eye.svg3
-rw-r--r--app/views/shared/icons/_icon_eye_slash.svg3
-rw-r--r--app/views/shared/icons/_icon_merged.svg3
-rw-r--r--app/views/shared/icons/_icon_pencil.svg3
-rw-r--r--app/views/shared/icons/_icon_random.svg3
-rw-r--r--app/views/shared/icons/_icon_resolve_discussion.svg1
-rw-r--r--app/views/shared/icons/_icon_status_closed.svg2
-rw-r--r--app/views/shared/icons/_icon_status_success_solid.svg1
-rw-r--r--app/views/shared/icons/_icon_tags.svg3
-rw-r--r--app/views/shared/icons/_icon_user.svg2
-rw-r--r--app/views/shared/icons/_mr_bold.svg3
-rw-r--r--app/views/shared/icons/_plus_square.svg1
-rw-r--r--app/views/shared/icons/_spring.svg (renamed from app/views/shared/icons/_java_spring.svg)0
-rw-r--r--app/views/shared/icons/_thumbs_up.svg1
-rw-r--r--app/views/shared/icons/_todo_done.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.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml12
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml11
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml19
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml25
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_user_dropdown_item.html.haml2
-rw-r--r--app/views/shared/issuable/form/_issue_assignee.html.haml2
-rw-r--r--app/views/shared/issuable/form/_merge_request_assignee.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml10
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml8
-rw-r--r--app/views/shared/milestones/_top.html.haml10
-rw-r--r--app/views/shared/notes/_note.html.haml4
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml4
-rw-r--r--app/views/shared/projects/_dropdown.html.haml5
-rw-r--r--app/views/shared/projects/_list.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml3
-rw-r--r--app/views/shared/repo/_repo.html.haml9
-rw-r--r--app/views/shared/snippets/_header.html.haml4
-rw-r--r--app/views/snippets/notes/_actions.html.haml17
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/workers/authorized_projects_worker.rb33
-rw-r--r--app/workers/build_coverage_worker.rb2
-rw-r--r--app/workers/build_finished_worker.rb4
-rw-r--r--app/workers/build_hooks_worker.rb4
-rw-r--r--app/workers/build_queue_worker.rb4
-rw-r--r--app/workers/build_success_worker.rb4
-rw-r--r--app/workers/concerns/build_queue.rb8
-rw-r--r--app/workers/concerns/exception_backtrace.rb8
-rw-r--r--app/workers/concerns/new_issuable.rb25
-rw-r--r--app/workers/concerns/pipeline_queue.rb12
-rw-r--r--app/workers/create_gpg_signature_worker.rb4
-rw-r--r--app/workers/expire_job_cache_worker.rb4
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb2
-rw-r--r--app/workers/git_garbage_collect_worker.rb34
-rw-r--r--app/workers/gitlab_shell_worker.rb2
-rw-r--r--app/workers/group_destroy_worker.rb1
-rw-r--r--app/workers/merge_worker.rb2
-rw-r--r--app/workers/namespaceless_project_destroy_worker.rb8
-rw-r--r--app/workers/new_issue_worker.rb2
-rw-r--r--app/workers/new_merge_request_worker.rb2
-rw-r--r--app/workers/pages_worker.rb2
-rw-r--r--app/workers/pipeline_hooks_worker.rb2
-rw-r--r--app/workers/pipeline_process_worker.rb2
-rw-r--r--app/workers/pipeline_success_worker.rb2
-rw-r--r--app/workers/pipeline_update_worker.rb2
-rw-r--r--app/workers/project_destroy_worker.rb1
-rw-r--r--app/workers/project_export_worker.rb1
-rw-r--r--app/workers/repository_fork_worker.rb16
-rw-r--r--app/workers/repository_import_worker.rb25
-rw-r--r--app/workers/stage_update_worker.rb12
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb2
-rw-r--r--app/workers/stuck_import_jobs_worker.rb50
-rwxr-xr-xbin/changelog134
-rwxr-xr-xbin/rspec-stackprof3
-rw-r--r--changelogs/unreleased/10085-stop-encoding-user-name.yml4
-rw-r--r--changelogs/unreleased/12968-generalize-profile-updates.yml4
-rw-r--r--changelogs/unreleased/13247-api_project_events_target_iid.yml4
-rw-r--r--changelogs/unreleased/13265-project_events_noteable_iid.yml4
-rw-r--r--changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml6
-rw-r--r--changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml5
-rw-r--r--changelogs/unreleased/1827-prevent-concurrent-editing-wiki.yml4
-rw-r--r--changelogs/unreleased/19629-remove-inactive-tokens-list.yml4
-rw-r--r--changelogs/unreleased/19650-remove-admin-section-from-search-results-if-user-doesnt-have-access.yml5
-rw-r--r--changelogs/unreleased/20817-please-add-coordinator-url-to-admin-area-runner-page.yml4
-rw-r--r--changelogs/unreleased/21949-add-type-to-changelog.yml4
-rw-r--r--changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml4
-rw-r--r--changelogs/unreleased/23036-replace-dashboard-event-filters-spinach.yml4
-rw-r--r--changelogs/unreleased/23036-replace-dashboard-spinach.yml4
-rw-r--r--changelogs/unreleased/23079-remove-default-scope-in-sortable.yml5
-rw-r--r--changelogs/unreleased/26372-duplicate-issue-slash-command.yml4
-rw-r--r--changelogs/unreleased/26692-predefined-variable-gitlab-user-name.yml5
-rw-r--r--changelogs/unreleased/27616-fix-contributions-graph-utc-offset-mysql.yml4
-rw-r--r--changelogs/unreleased/28202_decrease_abc_threshold_step2.yml4
-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/28472-ignore-auto-generated-mails.yml4
-rw-r--r--changelogs/unreleased/28938-password-change-workflow-for-admins.yml5
-rw-r--r--changelogs/unreleased/29289-project-destroy-clean-up-after-failure.yml4
-rw-r--r--changelogs/unreleased/29385-add_shrug_command.yml4
-rw-r--r--changelogs/unreleased/29811-fix-line-number-alignment.yml4
-rw-r--r--changelogs/unreleased/29901-refactor-initialization-dropzone_input-js.yml4
-rw-r--r--changelogs/unreleased/30162-retire-koding-integration.yml4
-rw-r--r--changelogs/unreleased/30634-protected-pipeline.yml5
-rw-r--r--changelogs/unreleased/31129-jira-project-key-elim.yml4
-rw-r--r--changelogs/unreleased/31207-clean-locked-merge-requests.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/31362_decrease_cyclomatic_complexity_threshold_step3.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/31533-usage-data-projects-stats.yml4
-rw-r--r--changelogs/unreleased/31571-don-t-let-webhooks-jobs-go-to-the-dead-jobs-queue.yml5
-rw-r--r--changelogs/unreleased/32483-jira-error.yml4
-rw-r--r--changelogs/unreleased/32665-refactor-project-visibility-settings.yml5
-rw-r--r--changelogs/unreleased/32844-issuables-performance.yml4
-rw-r--r--changelogs/unreleased/33095-mr-widget-ui.yml4
-rw-r--r--changelogs/unreleased/33097-issue-tracker.yml4
-rw-r--r--changelogs/unreleased/33601-add-csrf-token-verification-to-api.yml4
-rw-r--r--changelogs/unreleased/33620-remove-events-from-notification_settings.yml4
-rw-r--r--changelogs/unreleased/33741-clarify-k8s-service-keys.yml5
-rw-r--r--changelogs/unreleased/33770-respect-blockquote-line-breaks.yml4
-rw-r--r--changelogs/unreleased/33874_confi.yml5
-rw-r--r--changelogs/unreleased/34027-add-icons-to-sidebar.yml4
-rw-r--r--changelogs/unreleased/34028-collapse-sidebar.yml4
-rw-r--r--changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml4
-rw-r--r--changelogs/unreleased/34075-pipelines-count-mt.yml5
-rw-r--r--changelogs/unreleased/34110-memory-usage-notice-doesn-t-link-anywhere.yml4
-rw-r--r--changelogs/unreleased/34261-move-move-to-sidebar.yml5
-rw-r--r--changelogs/unreleased/34361-lazy-load-images-on-the-frontend.yml4
-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/34492-firefox-job.yml4
-rw-r--r--changelogs/unreleased/34509-improves-markdown-rendering-performance-for-commits-list.yml5
-rw-r--r--changelogs/unreleased/34519-extend-api-group-secret-variable.yml4
-rw-r--r--changelogs/unreleased/34534-update-vue-resource.yml4
-rw-r--r--changelogs/unreleased/34549-extract-devise-mappings-into-helper.yml4
-rw-r--r--changelogs/unreleased/34563-usage-ping-github.yml4
-rw-r--r--changelogs/unreleased/34643-fix-project-path-slugify.yml4
-rw-r--r--changelogs/unreleased/34764-rename-to-overview.yml4
-rw-r--r--changelogs/unreleased/34810-vue-pagination.yml4
-rw-r--r--changelogs/unreleased/34831-remove-coffee-rails-gem.yml4
-rw-r--r--changelogs/unreleased/34858-bump-scss-lint-to-0-54-0.yml4
-rw-r--r--changelogs/unreleased/34867-remove-net-ssh-gem.yml4
-rw-r--r--changelogs/unreleased/34869-bump-rubocop-to-0-49-1-and-rubocop-rspec-to-1-15-1.yml4
-rw-r--r--changelogs/unreleased/34921-global-dropdown-ui-improvement.yml4
-rw-r--r--changelogs/unreleased/34927-protect-manual-actions-on-tags.yml4
-rw-r--r--changelogs/unreleased/34945-readme-div-id.yml5
-rw-r--r--changelogs/unreleased/34978-remove-public-ci-favicon-ico.yml4
-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/35044-projects-logo-are-not-centered-vertically-on-projects-page.yml4
-rw-r--r--changelogs/unreleased/35048-empty-badges.yml5
-rw-r--r--changelogs/unreleased/35136-barchart-not-display-label-at-0-hour.yml4
-rw-r--r--changelogs/unreleased/35155-upgrade-fog-core-to-1-44-3-and-its-providers-to-the-latest.yml4
-rw-r--r--changelogs/unreleased/35161_first_time_contributor_badge.yml4
-rw-r--r--changelogs/unreleased/35163-url-in-commit-message-can-be-broken-in-blame.yml4
-rw-r--r--changelogs/unreleased/35164-cycle-analytics-firefox.yml4
-rw-r--r--changelogs/unreleased/35181-cannot-create-label-from-board-page.yml4
-rw-r--r--changelogs/unreleased/35191-prioritized-labels-for-non-member.yml4
-rw-r--r--changelogs/unreleased/35204-doc-api-ci-lint-typo.yml4
-rw-r--r--changelogs/unreleased/35225-transient-poll.yml4
-rw-r--r--changelogs/unreleased/35232-next-unresolved.yml4
-rw-r--r--changelogs/unreleased/35253-desc-protected-branches-for-non-member.yml4
-rw-r--r--changelogs/unreleased/35343-inherit-milestones-and-labels.yml5
-rw-r--r--changelogs/unreleased/35391-fix-star-i18n-in-js.yml4
-rw-r--r--changelogs/unreleased/35408-group-auto-avatars.yml4
-rw-r--r--changelogs/unreleased/35441-fix-division-by-zero.yml5
-rw-r--r--changelogs/unreleased/35483-improve-mobile-sidebar.yml4
-rw-r--r--changelogs/unreleased/35659-rename-pipeline.yml4
-rw-r--r--changelogs/unreleased/35686-unescape-wiki-title.yml5
-rw-r--r--changelogs/unreleased/35695-comment-appears-in-a-wrong-place-after-changing-diff-view-to-inline.yml4
-rw-r--r--changelogs/unreleased/35697-allow-logged-in-user-to-read-user-list.yml4
-rw-r--r--changelogs/unreleased/35721-auth-style-confirmation.yml5
-rw-r--r--changelogs/unreleased/35761-convdev-perc.yml4
-rw-r--r--changelogs/unreleased/35769-fix-ruby-2-4-compatibility.yml4
-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/35815-webhook-log-encoding-error.yml4
-rw-r--r--changelogs/unreleased/35845-improve-subgroup-creation-permissions.yml5
-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/36213-return-is_admin-in-users-api-when-current_user-is-admin.yml6
-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/36611-error-in-getcomposer-link.yml5
-rw-r--r--changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.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/36859-update-gpg-docs-with-gpg2.yml5
-rw-r--r--changelogs/unreleased/36860-migrate-issues-author.yml5
-rw-r--r--changelogs/unreleased/3686_make_tarball_download_url.yml4
-rw-r--r--changelogs/unreleased/36882-disable-gitlab-project-import-button-if-source-disabled.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/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/37104-fix-graph-date-format.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/37288-fix-wrong-header-when-testing-webhook.yml5
-rw-r--r--changelogs/unreleased/37331-button-MR-widget.yml5
-rw-r--r--changelogs/unreleased/37368-blob-viewer-on-mobile.yml5
-rw-r--r--changelogs/unreleased/37406-success-status-icon.yml5
-rw-r--r--changelogs/unreleased/37499-add-description-template-examples-to-the-docs.yml5
-rw-r--r--changelogs/unreleased/5971-webhook-testing.yml4
-rw-r--r--changelogs/unreleased/add-filter-by-my-reaction.yml4
-rw-r--r--changelogs/unreleased/add-filtered-search-group-issues-ce.yml4
-rw-r--r--changelogs/unreleased/add-star-for-action-scope.yml4
-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/additional-time-series-charts.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/artifacts-download-dropdown-menu-is-too-narrow.yml4
-rw-r--r--changelogs/unreleased/backstage-gb-after-save-asynchronous-job-hooks.yml5
-rw-r--r--changelogs/unreleased/breadcrumbs-collapsed-title-width-fix.yml4
-rw-r--r--changelogs/unreleased/bump-omniauth-ldap-gem-version-2-0-4.yml4
-rw-r--r--changelogs/unreleased/bump-omniauth-ldap-gem-version.yml4
-rw-r--r--changelogs/unreleased/bvl-add-all-settings-to-api.yml4
-rw-r--r--changelogs/unreleased/bvl-free-unused-names.yml5
-rw-r--r--changelogs/unreleased/bvl-improve-bare-project-import.yml6
-rw-r--r--changelogs/unreleased/bvl-nfs-circuitbreaker.yml4
-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/check-trigger-permissions.yml5
-rw-r--r--changelogs/unreleased/collapsable-pipeline-settings.yml5
-rw-r--r--changelogs/unreleased/diff-changed-files-dropdown.yml4
-rw-r--r--changelogs/unreleased/disable-project-export.yml4
-rw-r--r--changelogs/unreleased/dm-large-push-performance.yml4
-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-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/dont-use-limit-offset-when-counting-projects.yml4
-rw-r--r--changelogs/unreleased/dz-fix-calendar-today.yml4
-rw-r--r--changelogs/unreleased/eager-load-project-creators-for-project-dashboards.yml4
-rw-r--r--changelogs/unreleased/enable-scss-lint-bang-format.yml4
-rw-r--r--changelogs/unreleased/enable-scss-lint-declaration-order.yml4
-rw-r--r--changelogs/unreleased/enable-scss-lint-import-path.yml4
-rw-r--r--changelogs/unreleased/enable-scss-lint-property-spelling.yml4
-rw-r--r--changelogs/unreleased/enable-scss-lint-space-after-comma.yml4
-rw-r--r--changelogs/unreleased/enable-scss-lint-unnecessary-parent-reference.yml4
-rw-r--r--changelogs/unreleased/ericy_ts-protected_branches_api.yml5
-rw-r--r--changelogs/unreleased/events-migration-cleanup.yml5
-rw-r--r--changelogs/unreleased/feature-backup-custom-path.yml4
-rw-r--r--changelogs/unreleased/feature-dependency-status-badge.yml5
-rw-r--r--changelogs/unreleased/feature-gb-auto-retry-failed-ci-job.yml4
-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-signed-commits.yml4
-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-37239-implement-failure_reason-on-ci_builds.yml5
-rw-r--r--changelogs/unreleased/fix-500-error-when-rendering-avatar-for-deleted-project-creator.yml4
-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-gb-handle-max-pages-artifacts-size-correctly.yml4
-rw-r--r--changelogs/unreleased/fix-group-milestone-link-in-issuable-sidebar.yml4
-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-npm-security-updates.yml5
-rw-r--r--changelogs/unreleased/fix-oauth-checkboxes.yml4
-rw-r--r--changelogs/unreleased/fix-replying-to-commit-comment-in-mr-from-fork.yml4
-rw-r--r--changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml5
-rw-r--r--changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml4
-rw-r--r--changelogs/unreleased/fix-stray-or-in-project-create-ui.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/fixes-for-internal-auth-disabled.yml4
-rw-r--r--changelogs/unreleased/font-weight-adjusted.yml5
-rw-r--r--changelogs/unreleased/fuzzy-issue-search.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_ref_exists.yml4
-rw-r--r--changelogs/unreleased/github.yml4
-rw-r--r--changelogs/unreleased/group-milestone-references-system-notes.yml4
-rw-r--r--changelogs/unreleased/group-mr-search-bar.yml5
-rw-r--r--changelogs/unreleased/group-new-issue.yml4
-rw-r--r--changelogs/unreleased/handle-reserved-words-for-oauth-usernames.yml4
-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/issue-api-my-reaction.yml5
-rw-r--r--changelogs/unreleased/issue-boards-breadcrumbs-container.yml5
-rw-r--r--changelogs/unreleased/mattermost_fixes.yml4
-rw-r--r--changelogs/unreleased/memoize-user-personal-projects-count.yml4
-rw-r--r--changelogs/unreleased/merge-issuable-reopened-into-opened-state.yml4
-rw-r--r--changelogs/unreleased/mk-default-ldap-verify-certificates-secure.yml5
-rw-r--r--changelogs/unreleased/mk-fix-deploy-key-deletion.yml4
-rw-r--r--changelogs/unreleased/mk-fix-wiki-backup.yml4
-rw-r--r--changelogs/unreleased/move-action.yml4
-rw-r--r--changelogs/unreleased/mr-branch-link-use-tree.yml4
-rw-r--r--changelogs/unreleased/mr-index-page-performance.yml5
-rw-r--r--changelogs/unreleased/pass-before-script-as-is.yml4
-rw-r--r--changelogs/unreleased/pawel-add-sidekiq-metrics-endpoint-32145.yml4
-rw-r--r--changelogs/unreleased/pawel-add_more_variables_to_additional_metrics-35267.yml4
-rw-r--r--changelogs/unreleased/perf-slow-issuable.yml6
-rw-r--r--changelogs/unreleased/post-upload-pack-opt-out.yml4
-rw-r--r--changelogs/unreleased/project-foreign-keys-without-errors.yml4
-rw-r--r--changelogs/unreleased/rc-fix-branches-api-endpoint.yml5
-rw-r--r--changelogs/unreleased/rc-fix-commits-api.yml5
-rw-r--r--changelogs/unreleased/rc-fix-tags-api.yml5
-rw-r--r--changelogs/unreleased/remove-nprogress-gleaning.yml4
-rw-r--r--changelogs/unreleased/remove-redundant-query-when-retrieving-recent-pushes.yml4
-rw-r--r--changelogs/unreleased/reorganise-issues-indexes-for-sorting.yml4
-rw-r--r--changelogs/unreleased/replace_spinach_search_code-feature.yml5
-rw-r--r--changelogs/unreleased/replace_spinach_spec_browse_files.yml4
-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/request-store-wrap.yml4
-rw-r--r--changelogs/unreleased/restrict-haml-javascript.yml4
-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/search-flickering.yml4
-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-structured-logging.yml4
-rw-r--r--changelogs/unreleased/sidebar-cache-updates.yml5
-rw-r--r--changelogs/unreleased/skip-oauth-authorization-for-trusted-applications.yml4
-rw-r--r--changelogs/unreleased/sm-cherry-pick-list-commits-in-message.yml5
-rw-r--r--changelogs/unreleased/tc-api-root-merge-requests.yml4
-rw-r--r--changelogs/unreleased/tc-fix-wildcard-protected-delete-merged.yml4
-rw-r--r--changelogs/unreleased/tc-issue-api-assignee.yml4
-rw-r--r--changelogs/unreleased/tc-no-todo-service-select.yml4
-rw-r--r--changelogs/unreleased/tc-remove-nonexisting-namespace-pending-delete-projects.yml5
-rw-r--r--changelogs/unreleased/toggle-new-project-import-description.yml4
-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/wiki_api.yml5
-rw-r--r--changelogs/unreleased/wiki_title.yml4
-rw-r--r--changelogs/unreleased/winh-derive-project-name.yml4
-rw-r--r--changelogs/unreleased/winh-dropdown-changelog-docs.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-delete-mm-team.yml4
-rw-r--r--changelogs/unreleased/zj-disable-pages-in-subgroups.yml5
-rw-r--r--changelogs/unreleased/zj-pipeline-badge-improvements.yml4
-rw-r--r--changelogs/unreleased/zj-project-templates.yml4
-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.rb17
-rw-r--r--config/dependency_decisions.yml12
-rw-r--r--config/gitlab.yml.example30
-rw-r--r--config/initializers/0_acts_as_taggable.rb2
-rw-r--r--config/initializers/0_inflections.rb7
-rw-r--r--config/initializers/1_settings.rb47
-rw-r--r--config/initializers/6_validations.rb4
-rw-r--r--config/initializers/7_prometheus_metrics.rb3
-rw-r--r--config/initializers/8_metrics.rb7
-rw-r--r--config/initializers/active_record_array_type_casting.rb20
-rw-r--r--config/initializers/active_record_locking.rb4
-rw-r--r--config/initializers/active_record_mysql_timestamp.rb30
-rw-r--r--config/initializers/fast_gettext.rb5
-rw-r--r--config/initializers/omniauth.rb6
-rw-r--r--config/initializers/sentry.rb5
-rw-r--r--config/initializers/session_store.rb3
-rw-r--r--config/initializers/static_files.rb10
-rw-r--r--config/initializers/trusted_proxies.rb2
-rw-r--r--config/initializers/workhorse_multipart.rb4
-rw-r--r--config/karma.config.js1
-rw-r--r--config/prometheus/additional_metrics.yml38
-rw-r--r--config/routes.rb14
-rw-r--r--config/routes/ci.rb4
-rw-r--r--config/routes/project.rb16
-rw-r--r--config/routes/repository.rb3
-rw-r--r--config/routes/uploads.rb4
-rw-r--r--config/sidekiq_queues.yml4
-rw-r--r--config/webpack.config.js19
-rw-r--r--db/fixtures/development/10_merge_requests.rb2
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb2
-rw-r--r--db/migrate/20140502125220_migrate_repo_size.rb2
-rw-r--r--db/migrate/20160713205315_add_domain_blacklist_to_application_settings.rb1
-rw-r--r--db/migrate/20160817133006_add_koding_to_application_settings.rb1
-rw-r--r--db/migrate/20161017125927_add_unique_index_to_labels.rb2
-rw-r--r--db/migrate/20161020075830_default_request_access_projects.rb2
-rw-r--r--db/migrate/20161020180657_add_minimum_key_length_to_application_settings.rb29
-rw-r--r--db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb1
-rw-r--r--db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb1
-rw-r--r--db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb1
-rw-r--r--db/migrate/20170316163800_rename_system_namespaces.rb231
-rw-r--r--db/migrate/20170316163845_move_uploads_to_system_dir.rb2
-rw-r--r--db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb1
-rw-r--r--db/migrate/20170608152747_prepare_events_table_for_push_events_migration.rb51
-rw-r--r--db/migrate/20170608152748_create_push_event_payloads_tables.rb46
-rw-r--r--db/migrate/20170711145320_add_status_to_ci_stages.rb9
-rw-r--r--db/migrate/20170717074009_move_system_upload_folder.rb10
-rw-r--r--db/migrate/20170720111708_add_lock_version_to_ci_stages.rb9
-rw-r--r--db/migrate/20170727123534_add_index_on_events_project_id_id.rb37
-rw-r--r--db/migrate/20170802013652_add_storage_fields_to_project.rb16
-rw-r--r--db/migrate/20170807071105_add_hashed_storage_to_settings.rb18
-rw-r--r--db/migrate/20170809133343_add_broadcast_messages_index.rb21
-rw-r--r--db/migrate/20170809134534_add_broadcast_message_not_null_constraints.rb29
-rw-r--r--db/migrate/20170809142252_cleanup_appearances_schema.rb33
-rw-r--r--db/migrate/20170809161910_add_project_export_enabled_to_application_settings.rb14
-rw-r--r--db/migrate/20170816133938_add_access_level_to_ci_runners.rb16
-rw-r--r--db/migrate/20170816133940_add_protected_to_ci_builds.rb7
-rw-r--r--db/migrate/20170816143940_add_protected_to_ci_pipelines.rb7
-rw-r--r--db/migrate/20170816153940_add_index_on_ci_builds_protected.rb15
-rw-r--r--db/migrate/20170816234252_add_theme_id_to_users.rb10
-rw-r--r--db/migrate/20170817123339_add_verification_status_to_gpg_signatures.rb20
-rw-r--r--db/migrate/20170820100558_correct_protected_tags_foreign_keys.rb35
-rw-r--r--db/migrate/20170820120108_create_user_synced_attributes_metadata.rb15
-rw-r--r--db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb15
-rw-r--r--db/migrate/20170824162758_allow_appearances_description_html_null.rb18
-rw-r--r--db/migrate/20170825104051_migrate_issues_to_ghost_user.rb37
-rw-r--r--db/migrate/20170825154015_resolve_outdated_diff_discussions.rb9
-rw-r--r--db/migrate/20170828093725_create_project_auto_dev_ops.rb19
-rw-r--r--db/migrate/20170828135939_migrate_user_external_mail_data.rb57
-rw-r--r--db/migrate/20170830125940_add_failure_reason_to_ci_builds.rb9
-rw-r--r--db/migrate/20170830130119_steal_remaining_event_migration_jobs.rb18
-rw-r--r--db/migrate/20170830131015_swap_event_migration_tables.rb23
-rw-r--r--db/migrate/20170831092813_add_config_source_to_pipelines.rb7
-rw-r--r--db/migrate/20170901071411_add_foreign_key_to_issue_author.rb14
-rw-r--r--db/migrate/20170905112933_add_resolved_by_push_to_notes.rb9
-rw-r--r--db/migrate/limits_to_mysql.rb1
-rw-r--r--db/post_migrate/20170317162059_update_upload_paths_to_system.rb2
-rw-r--r--db/post_migrate/20170406111121_clean_upload_symlinks.rb2
-rw-r--r--db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb2
-rw-r--r--db/post_migrate/20170503004427_update_retried_for_ci_build.rb6
-rw-r--r--db/post_migrate/20170523083112_migrate_old_artifacts.rb6
-rw-r--r--db/post_migrate/20170606202615_move_appearance_to_system_dir.rb2
-rw-r--r--db/post_migrate/20170612071012_move_personal_snippets_files.rb4
-rw-r--r--db/post_migrate/20170627101016_schedule_event_migrations.rb40
-rw-r--r--db/post_migrate/20170711145558_migrate_stages_statuses.rb33
-rw-r--r--db/post_migrate/20170807190736_move_personal_snippet_files_into_correct_folder.rb29
-rw-r--r--db/post_migrate/20170815060945_remove_duplicate_mr_events.rb26
-rw-r--r--db/post_migrate/20170816102555_cleanup_nonexisting_namespace_pending_delete_projects.rb54
-rw-r--r--db/post_migrate/20170822101017_migrate_pipeline_sidekiq_queues.rb17
-rw-r--r--db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb57
-rw-r--r--db/post_migrate/20170828170513_remove_user_email_provider_column.rb12
-rw-r--r--db/post_migrate/20170828170516_remove_user_external_mail_columns.rb12
-rw-r--r--db/post_migrate/20170830084744_destroy_gpg_signatures.rb10
-rw-r--r--db/post_migrate/20170830150306_drop_events_for_migration_table.rb48
-rw-r--r--db/post_migrate/20170831195038_remove_valid_signature_from_gpg_signatures.rb11
-rw-r--r--db/schema.rb134
-rw-r--r--doc/README.md22
-rw-r--r--doc/administration/auth/authentiq.md2
-rw-r--r--doc/administration/auth/ldap.md9
-rw-r--r--doc/administration/high_availability/nfs.md4
-rw-r--r--doc/administration/integration/koding.md6
-rw-r--r--doc/administration/integration/plantuml.md11
-rw-r--r--doc/administration/logs.md27
-rw-r--r--doc/administration/monitoring/performance/performance_bar.md16
-rw-r--r--doc/api/README.md24
-rw-r--r--doc/api/ci/README.md24
-rw-r--r--doc/api/ci/builds.md147
-rw-r--r--doc/api/ci/lint.md51
-rw-r--r--doc/api/ci/runners.md59
-rw-r--r--doc/api/deploy_keys.md2
-rw-r--r--doc/api/environments.md2
-rw-r--r--doc/api/events.md42
-rw-r--r--doc/api/group_level_variables.md4
-rw-r--r--doc/api/group_milestones.md5
-rw-r--r--doc/api/groups.md9
-rw-r--r--doc/api/issues.md153
-rw-r--r--doc/api/jobs.md38
-rw-r--r--doc/api/keys.md1
-rw-r--r--doc/api/lint.md52
-rw-r--r--doc/api/merge_requests.md123
-rw-r--r--doc/api/namespaces.md4
-rw-r--r--doc/api/pipeline_schedules.md91
-rw-r--r--doc/api/project_snippets.md5
-rw-r--r--doc/api/projects.md239
-rw-r--r--doc/api/runners.md9
-rw-r--r--doc/api/session.md1
-rw-r--r--doc/api/settings.md16
-rw-r--r--doc/api/users.md229
-rw-r--r--doc/api/wikis.md157
-rw-r--r--doc/articles/artifactory_and_gitlab/index.md278
-rw-r--r--doc/articles/how_to_configure_ldap_gitlab_ce/index.md2
-rw-r--r--doc/articles/how_to_install_git/index.md2
-rw-r--r--doc/articles/index.md110
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_checkbox.pngbin0 -> 4730 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_page_empty_image.pngbin0 -> 56091 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_page_with_image.jpgbin0 -> 93531 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/deploy_keys_page.pngbin0 -> 339666 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/environment_page.pngbin0 -> 185393 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/environments_page.pngbin0 -> 134742 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/laravel_welcome_page.pngbin0 -> 5785 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.pngbin0 -> 177704 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/pipeline_page.pngbin0 -> 172664 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/pipelines_page.pngbin0 -> 119955 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/pipelines_page_deploy_button.pngbin0 -> 141393 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/production_server_app_directory.pngbin0 -> 11082 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/production_server_current_directory.pngbin0 -> 21993 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/img/secret_variables_page.pngbin0 -> 233764 bytes
-rw-r--r--doc/articles/laravel_with_gitlab_and_envoy/index.md680
-rw-r--r--doc/articles/numerous_undo_possibilities_in_git/img/branching.pngbin0 -> 26245 bytes
-rw-r--r--doc/articles/numerous_undo_possibilities_in_git/img/rebase_reset.pngbin0 -> 43609 bytes
-rw-r--r--doc/articles/numerous_undo_possibilities_in_git/img/revert.pngbin0 -> 28112 bytes
-rw-r--r--doc/articles/numerous_undo_possibilities_in_git/index.md497
-rw-r--r--doc/articles/openshift_and_gitlab/index.md2
-rw-r--r--doc/ci/README.md8
-rw-r--r--doc/ci/api/README.md1
-rw-r--r--doc/ci/api/builds.md1
-rw-r--r--doc/ci/api/runners.md1
-rw-r--r--doc/ci/autodeploy/img/auto_deploy_btn.pngbin0 -> 46825 bytes
-rw-r--r--doc/ci/autodeploy/img/auto_deploy_dropdown.pngbin99422 -> 75456 bytes
-rw-r--r--doc/ci/autodeploy/img/auto_monitoring.pngbin0 -> 89206 bytes
-rw-r--r--doc/ci/autodeploy/img/guide_connect_cluster.pngbin0 -> 38724 bytes
-rw-r--r--doc/ci/autodeploy/img/guide_integration.pngbin0 -> 44263 bytes
-rw-r--r--doc/ci/autodeploy/img/guide_secret.pngbin0 -> 16233 bytes
-rw-r--r--doc/ci/autodeploy/index.md84
-rw-r--r--doc/ci/autodeploy/quick_start_guide.md95
-rw-r--r--doc/ci/docker/using_docker_build.md10
-rw-r--r--doc/ci/docker/using_docker_images.md82
-rw-r--r--doc/ci/environments.md9
-rw-r--r--doc/ci/examples/README.md5
-rw-r--r--doc/ci/examples/code_climate.md6
-rw-r--r--doc/ci/pipelines.md24
-rw-r--r--doc/ci/runners/README.md39
-rw-r--r--doc/ci/runners/img/protected_runners_check_box.pngbin0 -> 8584 bytes
-rw-r--r--doc/ci/ssh_keys/README.md2
-rw-r--r--doc/ci/variables/README.md8
-rw-r--r--doc/ci/yaml/README.md42
-rw-r--r--doc/development/README.md4
-rw-r--r--doc/development/changelog.md54
-rw-r--r--doc/development/database_merge_request_checklist.md15
-rw-r--r--doc/development/doc_styleguide.md13
-rw-r--r--doc/development/fe_guide/style_guide_js.md120
-rw-r--r--doc/development/fe_guide/vue.md291
-rw-r--r--doc/development/hash_indexes.md20
-rw-r--r--doc/development/i18n_guide.md41
-rw-r--r--doc/development/img/manual_build_docs.pngbin0 -> 14869 bytes
-rw-r--r--doc/development/licensing.md7
-rw-r--r--doc/development/ordering_table_columns.md127
-rw-r--r--doc/development/serializing_data.md3
-rw-r--r--doc/development/sql.md26
-rw-r--r--doc/development/testing.md80
-rw-r--r--doc/development/verifying_database_capabilities.md26
-rw-r--r--doc/development/writing_documentation.md21
-rw-r--r--doc/gitlab-basics/create-project.md15
-rw-r--r--doc/gitlab-basics/img/create_new_project_info.pngbin20385 -> 82725 bytes
-rw-r--r--doc/install/README.md12
-rw-r--r--doc/install/azure/img/azure-add-inbound-sec-rule-http.pngbin0 -> 16927 bytes
-rw-r--r--doc/install/azure/img/azure-add-inbound-sec-rule-ssh.pngbin0 -> 16817 bytes
-rw-r--r--doc/install/azure/img/azure-create-virtual-machine-basics-password.pngbin0 -> 24862 bytes
-rw-r--r--doc/install/azure/img/azure-create-virtual-machine-basics.pngbin0 -> 22925 bytes
-rw-r--r--doc/install/azure/img/azure-create-virtual-machine-deployment.pngbin0 -> 23707 bytes
-rw-r--r--doc/install/azure/img/azure-create-virtual-machine-purchase.pngbin0 -> 43698 bytes
-rw-r--r--doc/install/azure/img/azure-create-virtual-machine-settings.pngbin0 -> 25686 bytes
-rw-r--r--doc/install/azure/img/azure-create-virtual-machine-size.pngbin0 -> 33322 bytes
-rw-r--r--doc/install/azure/img/azure-dashboard-highlight-nsg.pngbin0 -> 34248 bytes
-rw-r--r--doc/install/azure/img/azure-dashboard-running-resources.pngbin0 -> 34306 bytes
-rw-r--r--doc/install/azure/img/azure-dashboard.pngbin0 -> 36396 bytes
-rw-r--r--doc/install/azure/img/azure-inbound-sec-rules-list.pngbin0 -> 24592 bytes
-rw-r--r--doc/install/azure/img/azure-new-gitlab-ce.pngbin0 -> 22701 bytes
-rw-r--r--doc/install/azure/img/azure-new-search-gitlab.pngbin0 -> 19034 bytes
-rw-r--r--doc/install/azure/img/azure-nsg-inbound-sec-rules-add-highlight.pngbin0 -> 22583 bytes
-rw-r--r--doc/install/azure/img/azure-nsg-inbound-sec-rules-highlight.pngbin0 -> 31485 bytes
-rw-r--r--doc/install/azure/img/azure-vm-domain-name.pngbin0 -> 23802 bytes
-rw-r--r--doc/install/azure/img/azure-vm-management-public-ip.pngbin0 -> 34991 bytes
-rw-r--r--doc/install/azure/img/azure-vm-management-settings-network-interfaces.pngbin0 -> 35849 bytes
-rw-r--r--doc/install/azure/img/azure-vm-management.pngbin0 -> 35363 bytes
-rw-r--r--doc/install/azure/img/gitlab-admin-area-9.4.0.pngbin0 -> 35249 bytes
-rw-r--r--doc/install/azure/img/gitlab-admin-area.pngbin0 -> 29333 bytes
-rw-r--r--doc/install/azure/img/gitlab-change-password.pngbin0 -> 24404 bytes
-rw-r--r--doc/install/azure/img/gitlab-home.pngbin0 -> 26302 bytes
-rw-r--r--doc/install/azure/img/gitlab-login.pngbin0 -> 24656 bytes
-rw-r--r--doc/install/azure/img/gitlab-new-project.pngbin0 -> 34164 bytes
-rw-r--r--doc/install/azure/img/gitlab-project-home-empty.pngbin0 -> 28375 bytes
-rw-r--r--doc/install/azure/img/gitlab-project-home-instructions.pngbin0 -> 31738 bytes
-rw-r--r--doc/install/azure/img/gitlab-ssh-update-in-progress.pngbin0 -> 32799 bytes
-rw-r--r--doc/install/azure/index.md439
-rw-r--r--doc/install/docker.md18
-rw-r--r--doc/install/installation.md19
-rw-r--r--doc/install/kubernetes/gitlab_chart.md21
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md171
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md29
-rw-r--r--doc/install/kubernetes/index.md61
-rw-r--r--doc/install/requirements.md8
-rw-r--r--doc/integration/README.md1
-rw-r--r--doc/integration/omniauth.md18
-rw-r--r--doc/raketasks/backup_restore.md5
-rw-r--r--doc/security/README.md1
-rw-r--r--doc/security/img/ssh_keys_restrictions_settings.pngbin0 -> 68496 bytes
-rw-r--r--doc/security/ssh_keys_restrictions.md19
-rw-r--r--doc/ssh/README.md32
-rw-r--r--doc/system_hooks/system_hooks.md14
-rw-r--r--doc/topics/authentication/index.md1
-rw-r--r--doc/topics/autodevops/img/auto_devops_settings.pngbin0 -> 31851 bytes
-rw-r--r--doc/topics/autodevops/img/auto_monitoring.pngbin0 -> 89206 bytes
-rw-r--r--doc/topics/autodevops/img/guide_connect_cluster.pngbin0 -> 38724 bytes
-rw-r--r--doc/topics/autodevops/img/guide_integration.pngbin0 -> 44263 bytes
-rw-r--r--doc/topics/autodevops/img/guide_secret.pngbin0 -> 16233 bytes
-rw-r--r--doc/topics/autodevops/index.md342
-rw-r--r--doc/topics/autodevops/quick_start_guide.md95
-rw-r--r--doc/topics/git/index.md1
-rw-r--r--doc/topics/index.md1
-rw-r--r--doc/update/8.17-to-9.0.md10
-rw-r--r--doc/update/9.0-to-9.1.md10
-rw-r--r--doc/update/9.1-to-9.2.md16
-rw-r--r--doc/update/9.2-to-9.3.md15
-rw-r--r--doc/update/9.3-to-9.4.md15
-rw-r--r--doc/update/9.4-to-9.5.md4
-rw-r--r--doc/update/patch_versions.md8
-rw-r--r--doc/user/discussions/img/automatically_resolve_outdated_discussions.pngbin0 -> 117604 bytes
-rw-r--r--doc/user/discussions/index.md18
-rw-r--r--doc/user/group/img/share_with_group_lock.pngbin18257 -> 21541 bytes
-rw-r--r--doc/user/group/index.md49
-rw-r--r--doc/user/group/subgroups/index.md7
-rw-r--r--doc/user/index.md99
-rw-r--r--doc/user/permissions.md162
-rw-r--r--doc/user/profile/img/profile_settings_dropdown.pngbin0 -> 4184 bytes
-rw-r--r--doc/user/project/container_registry.md5
-rw-r--r--doc/user/project/description_templates.md54
-rw-r--r--doc/user/project/gpg_signed_commits/index.md1
-rw-r--r--doc/user/project/import/bitbucket.md62
-rw-r--r--doc/user/project/import/clearcase.md47
-rw-r--r--doc/user/project/import/cvs.md68
-rw-r--r--doc/user/project/import/fogbugz.md28
-rw-r--r--doc/user/project/import/gitea.md77
-rw-r--r--doc/user/project/import/github.md122
-rw-r--r--doc/user/project/import/gitlab_com.md20
-rw-r--r--doc/user/project/import/img/bitbucket_import_grant_access.png (renamed from doc/workflow/importing/img/bitbucket_import_grant_access.png)bin7248 -> 7248 bytes
-rw-r--r--doc/user/project/import/img/bitbucket_import_new_project.png (renamed from doc/workflow/importing/img/bitbucket_import_new_project.png)bin1316 -> 1316 bytes
-rw-r--r--doc/user/project/import/img/bitbucket_import_select_project.png (renamed from doc/workflow/importing/img/bitbucket_import_select_project.png)bin8688 -> 8688 bytes
-rw-r--r--doc/user/project/import/img/fogbugz_import_finished.png (renamed from doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png)bin17744 -> 17744 bytes
-rw-r--r--doc/user/project/import/img/fogbugz_import_login.png (renamed from doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png)bin13751 -> 13751 bytes
-rw-r--r--doc/user/project/import/img/fogbugz_import_select_fogbogz.png (renamed from doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png)bin12289 -> 12289 bytes
-rw-r--r--doc/user/project/import/img/fogbugz_import_select_project.png (renamed from doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png)bin20905 -> 20905 bytes
-rw-r--r--doc/user/project/import/img/fogbugz_import_user_map.png (renamed from doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png)bin51238 -> 51238 bytes
-rw-r--r--doc/user/project/import/img/gitlab_importer.png (renamed from doc/workflow/importing/gitlab_importer/importer.png)bin12864 -> 12864 bytes
-rw-r--r--doc/user/project/import/img/gitlab_new_project_page.png (renamed from doc/workflow/importing/gitlab_importer/new_project_page.png)bin21251 -> 21251 bytes
-rw-r--r--doc/user/project/import/img/import_projects_from_gitea_new_import.png (renamed from doc/workflow/importing/img/import_projects_from_gitea_new_import.png)bin15561 -> 15561 bytes
-rw-r--r--doc/user/project/import/img/import_projects_from_github_importer.png (renamed from doc/workflow/importing/img/import_projects_from_github_importer.png)bin17953 -> 17953 bytes
-rw-r--r--doc/user/project/import/img/import_projects_from_github_select_auth_method.png (renamed from doc/workflow/importing/img/import_projects_from_github_select_auth_method.png)bin17612 -> 17612 bytes
-rw-r--r--doc/user/project/import/img/import_projects_from_new_project_page.png (renamed from doc/workflow/importing/img/import_projects_from_new_project_page.png)bin36821 -> 36821 bytes
-rw-r--r--doc/user/project/import/index.md24
-rw-r--r--doc/user/project/import/perforce.md50
-rw-r--r--doc/user/project/import/svn.md183
-rw-r--r--doc/user/project/import/tfs.md42
-rw-r--r--doc/user/project/index.md19
-rw-r--r--doc/user/project/integrations/img/jira_service_page.pngbin83466 -> 193364 bytes
-rw-r--r--doc/user/project/integrations/jira.md8
-rw-r--r--doc/user/project/integrations/prometheus.md6
-rw-r--r--doc/user/project/integrations/prometheus_library/kubernetes.md2
-rw-r--r--doc/user/project/integrations/prometheus_library/metrics.md6
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx.md4
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx_ingress.md25
-rw-r--r--doc/user/project/issues/confidential_issues.md24
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/confidential_issues_index_page.pngbin8349 -> 107117 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/confidential_issues_issue_page.pngbin14230 -> 25354 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/confidential_issues_system_notes.pngbin2330 -> 4214 bytes
-rw-r--r--doc/user/project/issues/img/group_issues_list_view.pngbin0 -> 265130 bytes
-rwxr-xr-xdoc/user/project/issues/img/issue_tracker.pngbin37037 -> 0 bytes
-rw-r--r--doc/user/project/issues/img/project_issues_list_view.pngbin0 -> 309131 bytes
-rwxr-xr-xdoc/user/project/issues/img/sidebar_confidential_issue.pngbin0 -> 10210 bytes
-rw-r--r--doc/user/project/issues/img/sidebar_move_issue.pngbin0 -> 54511 bytes
-rwxr-xr-xdoc/user/project/issues/img/sidebar_not_confidential_issue.pngbin0 -> 8163 bytes
-rw-r--r--doc/user/project/issues/img/turn_off_confidentiality.pngbin0 -> 27307 bytes
-rw-r--r--doc/user/project/issues/img/turn_on_confidentiality.pngbin0 -> 33499 bytes
-rw-r--r--doc/user/project/issues/index.md34
-rw-r--r--doc/user/project/issues/moving_issues.md10
-rw-r--r--doc/user/project/koding.md5
-rw-r--r--doc/user/project/members/img/access_requests_management.png (renamed from doc/workflow/add-user/img/access_requests_management.png)bin11018 -> 11018 bytes
-rw-r--r--doc/user/project/members/img/add_new_user_to_project_settings.png (renamed from doc/workflow/add-user/img/add_new_user_to_project_settings.png)bin11046 -> 11046 bytes
-rw-r--r--doc/user/project/members/img/add_user_email_accept.png (renamed from doc/workflow/add-user/img/add_user_email_accept.png)bin16890 -> 16890 bytes
-rw-r--r--doc/user/project/members/img/add_user_email_ready.png (renamed from doc/workflow/add-user/img/add_user_email_ready.png)bin28171 -> 28171 bytes
-rw-r--r--doc/user/project/members/img/add_user_email_search.png (renamed from doc/workflow/add-user/img/add_user_email_search.png)bin29628 -> 29628 bytes
-rw-r--r--doc/user/project/members/img/add_user_give_permissions.png (renamed from doc/workflow/add-user/img/add_user_give_permissions.png)bin36619 -> 36619 bytes
-rw-r--r--doc/user/project/members/img/add_user_import_members_from_another_project.png (renamed from doc/workflow/add-user/img/add_user_import_members_from_another_project.png)bin25343 -> 25343 bytes
-rw-r--r--doc/user/project/members/img/add_user_imported_members.png (renamed from doc/workflow/add-user/img/add_user_imported_members.png)bin25398 -> 25398 bytes
-rw-r--r--doc/user/project/members/img/add_user_list_members.png (renamed from doc/workflow/add-user/img/add_user_list_members.png)bin16916 -> 16916 bytes
-rw-r--r--doc/user/project/members/img/add_user_members_menu.png (renamed from doc/workflow/add-user/img/add_user_members_menu.png)bin28994 -> 28994 bytes
-rw-r--r--doc/user/project/members/img/add_user_search_people.png (renamed from doc/workflow/add-user/img/add_user_search_people.png)bin25368 -> 25368 bytes
-rw-r--r--doc/user/project/members/img/max_access_level.png (renamed from doc/workflow/groups/max_access_level.png)bin34718 -> 34718 bytes
-rw-r--r--doc/user/project/members/img/other_group_sees_shared_project.pngbin0 -> 21154 bytes
-rw-r--r--doc/user/project/members/img/request_access_button.png (renamed from doc/workflow/add-user/img/request_access_button.png)bin25281 -> 25281 bytes
-rw-r--r--doc/user/project/members/img/share_project_with_groups.pngbin0 -> 37405 bytes
-rw-r--r--doc/user/project/members/img/share_project_with_groups_tab.pngbin0 -> 36482 bytes
-rw-r--r--doc/user/project/members/img/withdraw_access_request_button.png (renamed from doc/workflow/add-user/img/withdraw_access_request_button.png)bin26135 -> 26135 bytes
-rw-r--r--doc/user/project/members/index.md116
-rw-r--r--doc/user/project/members/share_project_with_groups.md43
-rw-r--r--doc/user/project/merge_requests/img/group_merge_requests_list_view.pngbin0 -> 283066 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_request_diff_file_navigation.pngbin0 -> 76052 bytes
-rw-r--r--doc/user/project/merge_requests/img/project_merge_requests_list_view.pngbin0 -> 325819 bytes
-rw-r--r--doc/user/project/merge_requests/index.md28
-rw-r--r--doc/user/project/pipelines/job_artifacts.md17
-rw-r--r--doc/user/project/pipelines/settings.md28
-rw-r--r--doc/user/project/protected_branches.md8
-rw-r--r--doc/user/project/quick_actions.md1
-rw-r--r--doc/user/project/repository/gpg_signed_commits/img/profile_settings_gpg_keys_paste_pub.png (renamed from doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_paste_pub.png)bin24514 -> 24514 bytes
-rw-r--r--doc/user/project/repository/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.pngbin0 -> 4403 bytes
-rw-r--r--doc/user/project/repository/gpg_signed_commits/img/project_signed_and_unsigned_commits.pngbin0 -> 113801 bytes
-rw-r--r--doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_unverified_signature.pngbin0 -> 12924 bytes
-rw-r--r--doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_verified_signature.pngbin0 -> 20652 bytes
-rw-r--r--doc/user/project/repository/gpg_signed_commits/index.md256
-rw-r--r--doc/user/project/repository/index.md10
-rw-r--r--doc/user/project/settings/import_export.md26
-rw-r--r--doc/user/project/wiki/index.md2
-rw-r--r--doc/user/reserved_names.md109
-rw-r--r--doc/user/search/img/group_issues_filter.pngbin0 -> 45288 bytes
-rw-r--r--doc/user/search/img/issue_search_by_term.pngbin0 -> 127492 bytes
-rw-r--r--doc/user/search/index.md28
-rw-r--r--doc/user/snippets.md2
-rw-r--r--doc/workflow/README.md7
-rw-r--r--doc/workflow/add-user/add-user.md115
-rw-r--r--doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys.pngbin32699 -> 0 bytes
-rw-r--r--doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.pngbin10331 -> 0 bytes
-rw-r--r--doc/workflow/gpg_signed_commits/img/project_signed_and_unsigned_commits.pngbin112812 -> 0 bytes
-rw-r--r--doc/workflow/gpg_signed_commits/img/project_signed_commit_unverified_signature.pngbin9542 -> 0 bytes
-rw-r--r--doc/workflow/gpg_signed_commits/img/project_signed_commit_verified_signature.pngbin14029 -> 0 bytes
-rw-r--r--doc/workflow/gpg_signed_commits/index.md84
-rw-r--r--doc/workflow/groups/other_group_sees_shared_project.pngbin30182 -> 0 bytes
-rw-r--r--doc/workflow/groups/share_project_with_groups.pngbin30307 -> 0 bytes
-rw-r--r--doc/workflow/importing/README.md18
-rw-r--r--doc/workflow/importing/import_projects_from_bitbucket.md63
-rw-r--r--doc/workflow/importing/import_projects_from_fogbugz.md30
-rw-r--r--doc/workflow/importing/import_projects_from_gitea.md78
-rw-r--r--doc/workflow/importing/import_projects_from_github.md123
-rw-r--r--doc/workflow/importing/import_projects_from_gitlab_com.md22
-rw-r--r--doc/workflow/importing/migrating_from_svn.md184
-rw-r--r--doc/workflow/project_features.md46
-rw-r--r--doc/workflow/share_projects_with_other_groups.md33
-rw-r--r--doc/workflow/share_with_group.md14
-rw-r--r--docker/README.md6
-rw-r--r--features/project/commits/user_lookup.feature16
-rw-r--r--features/project/source/search_code.feature15
-rw-r--r--features/project/star.feature39
-rw-r--r--features/steps/explore/projects.rb4
-rw-r--r--features/steps/group/milestones.rb4
-rw-r--r--features/steps/profile/emails.rb2
-rw-r--r--features/steps/project/active_tab.rb16
-rw-r--r--features/steps/project/commits/user_lookup.rb49
-rw-r--r--features/steps/project/fork.rb6
-rw-r--r--features/steps/project/graph.rb6
-rw-r--r--features/steps/project/issues/issues.rb1
-rw-r--r--features/steps/project/issues/milestones.rb4
-rw-r--r--features/steps/project/merge_requests.rb5
-rw-r--r--features/steps/project/pages.rb11
-rw-r--r--features/steps/project/project.rb1
-rw-r--r--features/steps/project/project_milestone.rb2
-rw-r--r--features/steps/project/redirects.rb2
-rw-r--r--features/steps/project/snippets.rb2
-rw-r--r--features/steps/project/source/markdown_render.rb2
-rw-r--r--features/steps/project/source/search_code.rb19
-rw-r--r--features/steps/project/star.rb37
-rw-r--r--features/steps/shared/active_tab.rb8
-rw-r--r--features/steps/shared/group.rb4
-rw-r--r--features/steps/shared/note.rb14
-rw-r--r--features/steps/shared/paths.rb4
-rw-r--r--features/steps/shared/project.rb30
-rw-r--r--features/steps/shared/project_tab.rb4
-rw-r--r--features/steps/user.rb13
-rw-r--r--features/support/gitaly.rb3
-rw-r--r--lib/after_commit_queue.rb30
-rw-r--r--lib/api/access_requests.rb10
-rw-r--r--lib/api/api.rb13
-rw-r--r--lib/api/api_guard.rb2
-rw-r--r--lib/api/award_emoji.rb5
-rw-r--r--lib/api/boards.rb12
-rw-r--r--lib/api/branches.rb40
-rw-r--r--lib/api/broadcast_messages.rb5
-rw-r--r--lib/api/commit_statuses.rb10
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/deploy_keys.rb5
-rw-r--r--lib/api/deployments.rb2
-rw-r--r--lib/api/entities.rb95
-rw-r--r--lib/api/environments.rb5
-rw-r--r--lib/api/events.rb2
-rw-r--r--lib/api/files.rb17
-rw-r--r--lib/api/group_milestones.rb2
-rw-r--r--lib/api/group_variables.rb5
-rw-r--r--lib/api/groups.rb34
-rw-r--r--lib/api/helpers.rb59
-rw-r--r--lib/api/helpers/internal_helpers.rb13
-rw-r--r--lib/api/helpers/members_helpers.rb4
-rw-r--r--lib/api/helpers/pagination.rb17
-rw-r--r--lib/api/helpers/runner.rb2
-rw-r--r--lib/api/internal.rb28
-rw-r--r--lib/api/issues.rb37
-rw-r--r--lib/api/job_artifacts.rb80
-rw-r--r--lib/api/jobs.rb84
-rw-r--r--lib/api/labels.rb5
-rw-r--r--lib/api/members.rb10
-rw-r--r--lib/api/merge_request_diffs.rb4
-rw-r--r--lib/api/merge_requests.rb9
-rw-r--r--lib/api/milestone_responses.rb2
-rw-r--r--lib/api/notes.rb10
-rw-r--r--lib/api/notification_settings.rb2
-rw-r--r--lib/api/pipeline_schedules.rb86
-rw-r--r--lib/api/pipelines.rb2
-rw-r--r--lib/api/project_hooks.rb5
-rw-r--r--lib/api/project_milestones.rb2
-rw-r--r--lib/api/project_snippets.rb6
-rw-r--r--lib/api/projects.rb20
-rw-r--r--lib/api/protected_branches.rb6
-rw-r--r--lib/api/repositories.rb2
-rw-r--r--lib/api/runner.rb12
-rw-r--r--lib/api/runners.rb15
-rw-r--r--lib/api/services.rb16
-rw-r--r--lib/api/settings.rb8
-rw-r--r--lib/api/snippets.rb3
-rw-r--r--lib/api/subscriptions.rb2
-rw-r--r--lib/api/system_hooks.rb3
-rw-r--r--lib/api/tags.rb15
-rw-r--r--lib/api/templates.rb6
-rw-r--r--lib/api/todos.rb2
-rw-r--r--lib/api/triggers.rb5
-rw-r--r--lib/api/users.rb187
-rw-r--r--lib/api/v3/builds.rb2
-rw-r--r--lib/api/v3/entities.rb17
-rw-r--r--lib/api/v3/merge_request_diffs.rb2
-rw-r--r--lib/api/v3/milestones.rb1
-rw-r--r--lib/api/v3/notes.rb6
-rw-r--r--lib/api/v3/projects.rb11
-rw-r--r--lib/api/v3/templates.rb6
-rw-r--r--lib/api/v3/triggers.rb32
-rw-r--r--lib/api/variables.rb3
-rw-r--r--lib/api/wikis.rb89
-rw-r--r--lib/backup/manager.rb4
-rw-r--r--lib/backup/repository.rb2
-rw-r--r--lib/banzai/commit_renderer.rb11
-rw-r--r--lib/banzai/filter/external_issue_reference_filter.rb4
-rw-r--r--lib/banzai/filter/image_lazy_load_filter.rb4
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb90
-rw-r--r--lib/banzai/object_renderer.rb4
-rw-r--r--lib/banzai/pipeline/base_pipeline.rb2
-rw-r--r--lib/banzai/renderer.rb8
-rw-r--r--lib/bitbucket/collection.rb2
-rw-r--r--lib/ci/ansi2html.rb6
-rw-r--r--lib/ci/api/api.rb39
-rw-r--r--lib/ci/api/builds.rb219
-rw-r--r--lib/ci/api/entities.rb93
-rw-r--r--lib/ci/api/helpers.rb89
-rw-r--r--lib/ci/api/runners.rb50
-rw-r--r--lib/ci/api/triggers.rb39
-rw-r--r--lib/ci/charts.rb2
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb49
-rw-r--r--lib/constraints/project_url_constrainer.rb2
-rw-r--r--lib/declarative_policy/base.rb4
-rw-r--r--lib/declarative_policy/dsl.rb2
-rw-r--r--lib/email_template_interceptor.rb2
-rw-r--r--lib/file_size_validator.rb4
-rw-r--r--lib/file_streamer.rb16
-rw-r--r--lib/github/import.rb84
-rw-r--r--lib/github/representation/branch.rb2
-rw-r--r--lib/gitlab/access.rb6
-rw-r--r--lib/gitlab/asciidoc.rb2
-rw-r--r--lib/gitlab/auth.rb10
-rw-r--r--lib/gitlab/auth/ip_rate_limiter.rb12
-rw-r--r--lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb16
-rw-r--r--lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb176
-rw-r--r--lib/gitlab/background_migration/migrate_stage_status.rb77
-rw-r--r--lib/gitlab/background_migration/move_personal_snippet_files.rb79
-rw-r--r--lib/gitlab/bare_repository_importer.rb96
-rw-r--r--lib/gitlab/cache/request_cache.rb2
-rw-r--r--lib/gitlab/checks/force_push.rb19
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb244
-rw-r--r--lib/gitlab/ci/build/artifacts/path.rb51
-rw-r--r--lib/gitlab/ci/config/entry/attributable.rb4
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb3
-rw-r--r--lib/gitlab/ci/config/entry/job.rb4
-rw-r--r--lib/gitlab/ci/config/entry/node.rb18
-rw-r--r--lib/gitlab/ci/config/entry/policy.rb53
-rw-r--r--lib/gitlab/ci/config/entry/simplifiable.rb43
-rw-r--r--lib/gitlab/ci/config/entry/trigger.rb18
-rw-r--r--lib/gitlab/ci/config/entry/validatable.rb11
-rw-r--r--lib/gitlab/ci/config/entry/validator.rb16
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb8
-rw-r--r--lib/gitlab/ci/stage/seed.rb9
-rw-r--r--lib/gitlab/conflict/file_collection.rb6
-rw-r--r--lib/gitlab/current_settings.rb2
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb2
-rw-r--r--lib/gitlab/data_builder/push.rb33
-rw-r--r--lib/gitlab/data_builder/repository.rb21
-rw-r--r--lib/gitlab/database.rb12
-rw-r--r--lib/gitlab/database/grant.rb34
-rw-r--r--lib/gitlab/database/migration_helpers.rb55
-rw-r--r--lib/gitlab/diff/file.rb11
-rw-r--r--lib/gitlab/diff/line.rb2
-rw-r--r--lib/gitlab/diff/line_mapper.rb6
-rw-r--r--lib/gitlab/ee_compat_check.rb5
-rw-r--r--lib/gitlab/encoding_helper.rb19
-rw-r--r--lib/gitlab/exclusive_lease.rb10
-rw-r--r--lib/gitlab/file_finder.rb43
-rw-r--r--lib/gitlab/git.rb1
-rw-r--r--lib/gitlab/git/blame.rb2
-rw-r--r--lib/gitlab/git/blob.rb16
-rw-r--r--lib/gitlab/git/commit.rb153
-rw-r--r--lib/gitlab/git/commit_stats.rb2
-rw-r--r--lib/gitlab/git/committer.rb21
-rw-r--r--lib/gitlab/git/diff.rb20
-rw-r--r--lib/gitlab/git/diff_collection.rb3
-rw-r--r--lib/gitlab/git/hook.rb17
-rw-r--r--lib/gitlab/git/hooks_service.rb36
-rw-r--r--lib/gitlab/git/operation_service.rb168
-rw-r--r--lib/gitlab/git/repository.rb525
-rw-r--r--lib/gitlab/git/tree.rb13
-rw-r--r--lib/gitlab/git_access.rb28
-rw-r--r--lib/gitlab/gitaly_client.rb42
-rw-r--r--lib/gitlab/gitaly_client/blob_service.rb13
-rw-r--r--lib/gitlab/gitaly_client/commit.rb14
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb88
-rw-r--r--lib/gitlab/gitaly_client/diff.rb4
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb68
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb23
-rw-r--r--lib/gitlab/gitaly_client/util.rb4
-rw-r--r--lib/gitlab/github_import/base_formatter.rb4
-rw-r--r--lib/gitlab/github_import/client.rb2
-rw-r--r--lib/gitlab/github_import/importer.rb4
-rw-r--r--lib/gitlab/gitlab_import/client.rb2
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/gpg.rb42
-rw-r--r--lib/gitlab/gpg/commit.rb48
-rw-r--r--lib/gitlab/gpg/invalid_gpg_signature_updater.rb6
-rw-r--r--lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb19
-rw-r--r--lib/gitlab/health_checks/db_check.rb2
-rw-r--r--lib/gitlab/health_checks/redis/cache_check.rb2
-rw-r--r--lib/gitlab/health_checks/redis/queues_check.rb2
-rw-r--r--lib/gitlab/health_checks/redis/redis_check.rb2
-rw-r--r--lib/gitlab/health_checks/redis/shared_state_check.rb2
-rw-r--r--lib/gitlab/health_checks/simple_abstract_check.rb8
-rw-r--r--lib/gitlab/i18n/metadata_entry.rb27
-rw-r--r--lib/gitlab/i18n/po_linter.rb214
-rw-r--r--lib/gitlab/i18n/translation_entry.rb92
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/attributes_finder.rb1
-rw-r--r--lib/gitlab/import_export/file_importer.rb6
-rw-r--r--lib/gitlab/import_export/import_export.yml35
-rw-r--r--lib/gitlab/import_export/merge_request_parser.rb2
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb90
-rw-r--r--lib/gitlab/import_export/relation_factory.rb2
-rw-r--r--lib/gitlab/import_export/shared.rb2
-rw-r--r--lib/gitlab/issuables_count_for_state.rb50
-rw-r--r--lib/gitlab/job_waiter.rb57
-rw-r--r--lib/gitlab/key_fingerprint.rb48
-rw-r--r--lib/gitlab/lazy.rb2
-rw-r--r--lib/gitlab/ldap/adapter.rb6
-rw-r--r--lib/gitlab/ldap/auth_hash.rb2
-rw-r--r--lib/gitlab/ldap/person.rb13
-rw-r--r--lib/gitlab/ldap/user.rb4
-rw-r--r--lib/gitlab/logger.rb8
-rw-r--r--lib/gitlab/markdown/pipeline.rb2
-rw-r--r--lib/gitlab/metrics/influx_db.rb2
-rw-r--r--lib/gitlab/middleware/rails_queue_duration.rb2
-rw-r--r--lib/gitlab/middleware/webpack_proxy.rb2
-rw-r--r--lib/gitlab/o_auth/auth_hash.rb17
-rw-r--r--lib/gitlab/o_auth/session.rb2
-rw-r--r--lib/gitlab/o_auth/user.rb32
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/gitlab/performance_bar.rb2
-rw-r--r--lib/gitlab/polling_interval.rb2
-rw-r--r--lib/gitlab/project_template.rb4
-rw-r--r--lib/gitlab/prometheus/queries/matched_metrics_query.rb4
-rw-r--r--lib/gitlab/protocol_access.rb2
-rw-r--r--lib/gitlab/recaptcha.rb2
-rw-r--r--lib/gitlab/redis/cache.rb5
-rw-r--r--lib/gitlab/redis/queues.rb5
-rw-r--r--lib/gitlab/redis/shared_state.rb5
-rw-r--r--lib/gitlab/redis/wrapper.rb13
-rw-r--r--lib/gitlab/reference_counter.rb44
-rw-r--r--lib/gitlab/reference_extractor.rb2
-rw-r--r--lib/gitlab/regex.rb3
-rw-r--r--lib/gitlab/saml/user.rb2
-rw-r--r--lib/gitlab/seeder.rb2
-rw-r--r--lib/gitlab/sentry.rb4
-rw-r--r--lib/gitlab/shell.rb67
-rw-r--r--lib/gitlab/sidekiq_status.rb9
-rw-r--r--lib/gitlab/sidekiq_throttler.rb2
-rw-r--r--lib/gitlab/slash_commands/presenters/help.rb2
-rw-r--r--lib/gitlab/sql/pattern.rb46
-rw-r--r--lib/gitlab/ssh_public_key.rb71
-rw-r--r--lib/gitlab/string_range_marker.rb34
-rw-r--r--lib/gitlab/template/base_template.rb6
-rw-r--r--lib/gitlab/themes.rb84
-rw-r--r--lib/gitlab/url_blocker.rb8
-rw-r--r--lib/gitlab/url_sanitizer.rb19
-rw-r--r--lib/gitlab/usage_data.rb4
-rw-r--r--lib/gitlab/utils.rb17
-rw-r--r--lib/gitlab/workhorse.rb10
-rw-r--r--lib/rspec_flaky/example.rb46
-rw-r--r--lib/rspec_flaky/flaky_example.rb39
-rw-r--r--lib/rspec_flaky/listener.rb75
-rw-r--r--lib/static_model.rb2
-rw-r--r--lib/support/nginx/gitlab35
-rw-r--r--lib/support/nginx/gitlab-pages5
-rw-r--r--lib/support/nginx/gitlab-pages-ssl5
-rw-r--r--lib/support/nginx/gitlab-ssl39
-rw-r--r--lib/system_check/app/git_config_check.rb2
-rw-r--r--lib/system_check/app/git_user_default_ssh_config_check.rb69
-rw-r--r--lib/system_check/app/init_script_up_to_date_check.rb28
-rw-r--r--lib/system_check/base_check.rb21
-rw-r--r--lib/system_check/incoming_email/foreman_configured_check.rb23
-rw-r--r--lib/system_check/incoming_email/imap_authentication_check.rb45
-rw-r--r--lib/system_check/incoming_email/initd_configured_check.rb32
-rw-r--r--lib/system_check/incoming_email/mail_room_running_check.rb43
-rw-r--r--lib/system_check/simple_executor.rb6
-rw-r--r--lib/tasks/gettext.rake42
-rw-r--r--lib/tasks/gitlab/check.rake138
-rw-r--r--lib/tasks/gitlab/cleanup.rake2
-rw-r--r--lib/tasks/gitlab/gitaly.rake8
-rw-r--r--lib/tasks/gitlab/helpers.rake2
-rw-r--r--lib/tasks/gitlab/import.rake65
-rw-r--r--lib/tasks/gitlab/import_export.rake11
-rw-r--r--lib/tasks/gitlab/task_helpers.rb6
-rw-r--r--lib/tasks/gitlab/update_templates.rake9
-rw-r--r--lib/tasks/import.rake20
-rw-r--r--lib/uploaded_file.rb2
-rw-r--r--locale/bg/gitlab.po580
-rw-r--r--locale/de/gitlab.po1315
-rw-r--r--locale/en/gitlab.po123
-rw-r--r--locale/eo/gitlab.po584
-rw-r--r--locale/es/gitlab.po388
-rw-r--r--locale/fr/gitlab.po631
-rw-r--r--locale/gitlab.pot363
-rw-r--r--locale/it/gitlab.po638
-rw-r--r--locale/ja/gitlab.po527
-rw-r--r--locale/ko/gitlab.po530
-rw-r--r--locale/pt_BR/gitlab.po609
-rw-r--r--locale/ru/gitlab.po594
-rw-r--r--locale/uk/gitlab.po585
-rw-r--r--locale/zh_CN/gitlab.po482
-rw-r--r--locale/zh_HK/gitlab.po481
-rw-r--r--locale/zh_TW/gitlab.po513
-rw-r--r--package.json6
-rw-r--r--public/404.html9
-rw-r--r--public/422.html6
-rw-r--r--public/500.html6
-rw-r--r--public/502.html6
-rw-r--r--public/503.html6
-rw-r--r--public/deploy.html6
-rw-r--r--qa/qa/runtime/release.rb2
-rw-r--r--rubocop/cop/migration/safer_boolean_column.rb94
-rw-r--r--rubocop/rubocop.rb1
-rwxr-xr-xscripts/detect-new-flaky-examples21
-rwxr-xr-xscripts/merge-reports2
-rwxr-xr-xscripts/static-analysis5
-rwxr-xr-xscripts/trigger-build-docs80
-rwxr-xr-xscripts/trigger-build-omnibus (renamed from scripts/trigger-build)0
-rw-r--r--spec/bin/changelog_spec.rb98
-rw-r--r--spec/config/mail_room_spec.rb4
-rw-r--r--spec/controllers/admin/projects_controller_spec.rb12
-rw-r--r--spec/controllers/admin/users_controller_spec.rb34
-rw-r--r--spec/controllers/application_controller_spec.rb13
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb169
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb235
-rw-r--r--spec/controllers/boards/lists_controller_spec.rb252
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb82
-rw-r--r--spec/controllers/groups_controller_spec.rb124
-rw-r--r--spec/controllers/invites_controller_spec.rb2
-rw-r--r--spec/controllers/passwords_controller_spec.rb8
-rw-r--r--spec/controllers/profiles/preferences_controller_spec.rb6
-rw-r--r--spec/controllers/profiles_controller_spec.rb25
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb8
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb221
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb252
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb234
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb22
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb17
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb13
-rw-r--r--spec/controllers/projects/protected_tags_controller_spec.rb17
-rw-r--r--spec/controllers/projects/services_controller_spec.rb42
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb4
-rw-r--r--spec/controllers/projects/todos_controller_spec.rb4
-rw-r--r--spec/controllers/projects_controller_spec.rb144
-rw-r--r--spec/controllers/registrations_controller_spec.rb4
-rw-r--r--spec/controllers/snippets/notes_controller_spec.rb6
-rw-r--r--spec/controllers/snippets_controller_spec.rb12
-rw-r--r--spec/controllers/uploads_controller_spec.rb4
-rw-r--r--spec/controllers/users_controller_spec.rb10
-rw-r--r--spec/factories/ci/builds.rb7
-rw-r--r--spec/factories/ci/pipeline_variables.rb (renamed from spec/factories/ci/pipeline_variable_variables.rb)0
-rw-r--r--spec/factories/ci/pipelines.rb5
-rw-r--r--spec/factories/ci/runners.rb5
-rw-r--r--spec/factories/ci/stages.rb8
-rw-r--r--spec/factories/ci/trigger_requests.rb9
-rw-r--r--spec/factories/ci/triggers.rb2
-rw-r--r--spec/factories/deployments.rb4
-rw-r--r--spec/factories/events.rb16
-rw-r--r--spec/factories/gpg_signature.rb2
-rw-r--r--spec/factories/keys.rb49
-rw-r--r--spec/factories/merge_requests.rb11
-rw-r--r--spec/factories/milestones.rb4
-rw-r--r--spec/factories/project_auto_devops.rb7
-rw-r--r--spec/factories/projects.rb105
-rw-r--r--spec/factories/users.rb6
-rw-r--r--spec/features/admin/admin_active_tab_spec.rb8
-rw-r--r--spec/features/admin/admin_browses_logs_spec.rb8
-rw-r--r--spec/features/admin/admin_hooks_spec.rb4
-rw-r--r--spec/features/admin/admin_projects_spec.rb8
-rw-r--r--spec/features/admin/admin_settings_spec.rb22
-rw-r--r--spec/features/atom/users_spec.rb6
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb4
-rw-r--r--spec/features/boards/boards_spec.rb22
-rw-r--r--spec/features/boards/sidebar_spec.rb12
-rw-r--r--spec/features/calendar_spec.rb18
-rw-r--r--spec/features/commits_spec.rb101
-rw-r--r--spec/features/cycle_analytics_spec.rb38
-rw-r--r--spec/features/dashboard/active_tab_spec.rb25
-rw-r--r--spec/features/dashboard/activity_spec.rb30
-rw-r--r--spec/features/dashboard/archived_projects_spec.rb7
-rw-r--r--spec/features/dashboard/projects_spec.rb28
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb2
-rw-r--r--spec/features/explore/new_menu_spec.rb2
-rw-r--r--spec/features/groups/group_name_toggle_spec.rb51
-rw-r--r--spec/features/groups/group_settings_spec.rb6
-rw-r--r--spec/features/groups/members/request_access_spec.rb2
-rw-r--r--spec/features/groups/merge_requests_spec.rb17
-rw-r--r--spec/features/groups/milestone_spec.rb39
-rw-r--r--spec/features/groups/share_lock_spec.rb62
-rw-r--r--spec/features/groups_spec.rb19
-rw-r--r--spec/features/help_pages_spec.rb2
-rw-r--r--spec/features/issues/award_emoji_spec.rb4
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb18
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb22
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb182
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb288
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb24
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb26
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb632
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb6
-rw-r--r--spec/features/issues/form_spec.rb6
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb44
-rw-r--r--spec/features/issues/markdown_toolbar_spec.rb20
-rw-r--r--spec/features/issues/move_spec.rb32
-rw-r--r--spec/features/issues/note_polling_spec.rb39
-rw-r--r--spec/features/issues/update_issues_spec.rb12
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb109
-rw-r--r--spec/features/issues_spec.rb47
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb10
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb12
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb8
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb5
-rw-r--r--spec/features/merge_requests/diffs_spec.rb4
-rw-r--r--spec/features/merge_requests/discussion_spec.rb4
-rw-r--r--spec/features/merge_requests/filter_by_labels_spec.rb10
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb2
-rw-r--r--spec/features/merge_requests/filter_merge_requests_spec.rb70
-rw-r--r--spec/features/merge_requests/form_spec.rb7
-rw-r--r--spec/features/merge_requests/merge_commit_message_toggle_spec.rb4
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb4
-rw-r--r--spec/features/merge_requests/reset_filters_spec.rb6
-rw-r--r--spec/features/merge_requests/resolve_outdated_diff_discussions.rb78
-rw-r--r--spec/features/merge_requests/update_merge_requests_spec.rb6
-rw-r--r--spec/features/merge_requests/user_lists_merge_requests_spec.rb16
-rw-r--r--spec/features/merge_requests/user_posts_diff_notes_spec.rb16
-rw-r--r--spec/features/merge_requests/user_posts_notes_spec.rb3
-rw-r--r--spec/features/milestones/show_spec.rb2
-rw-r--r--spec/features/participants_autocomplete_spec.rb14
-rw-r--r--spec/features/password_reset_spec.rb4
-rw-r--r--spec/features/profile_spec.rb15
-rw-r--r--spec/features/profiles/account_spec.rb6
-rw-r--r--spec/features/profiles/gpg_keys_spec.rb4
-rw-r--r--spec/features/profiles/keys_spec.rb17
-rw-r--r--spec/features/profiles/password_spec.rb4
-rw-r--r--spec/features/profiles/preferences_spec.rb28
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb18
-rw-r--r--spec/features/projects/commits/rss_spec.rb (renamed from spec/features/projects/commit/rss_spec.rb)0
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb44
-rw-r--r--spec/features/projects/edit_spec.rb31
-rw-r--r--spec/features/projects/features_visibility_spec.rb43
-rw-r--r--spec/features/projects/files/creating_a_file_spec.rb2
-rw-r--r--spec/features/projects/files/find_files_spec.rb23
-rw-r--r--spec/features/projects/files/undo_template_spec.rb2
-rw-r--r--spec/features/projects/files/user_searches_for_files_spec.rb58
-rw-r--r--spec/features/projects/group_links_spec.rb77
-rw-r--r--spec/features/projects/guest_navigation_menu_spec.rb4
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb89
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin681481 -> 679559 bytes
-rw-r--r--spec/features/projects/issuable_counts_caching_spec.rb132
-rw-r--r--spec/features/projects/jobs_spec.rb40
-rw-r--r--spec/features/projects/members/group_links_spec.rb69
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb69
-rw-r--r--spec/features/projects/members/share_with_group_spec.rb191
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb5
-rw-r--r--spec/features/projects/new_project_spec.rb55
-rw-r--r--spec/features/projects/project_settings_spec.rb20
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb6
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb10
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb19
-rw-r--r--spec/features/projects/sub_group_issuables_spec.rb5
-rw-r--r--spec/features/projects/user_creates_files_spec.rb3
-rw-r--r--spec/features/projects/user_edits_files_spec.rb26
-rw-r--r--spec/features/projects/user_interacts_with_stars_spec.rb38
-rw-r--r--spec/features/projects_spec.rb45
-rw-r--r--spec/features/protected_tags_spec.rb2
-rw-r--r--spec/features/reportable_note/commit_spec.rb4
-rw-r--r--spec/features/reportable_note/issue_spec.rb2
-rw-r--r--spec/features/reportable_note/merge_request_spec.rb4
-rw-r--r--spec/features/reportable_note/snippets_spec.rb2
-rw-r--r--spec/features/runners_spec.rb211
-rw-r--r--spec/features/search_spec.rb40
-rw-r--r--spec/features/signed_commits_spec.rb179
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb6
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb6
-rw-r--r--spec/features/snippets/user_edits_snippet_spec.rb2
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb4
-rw-r--r--spec/features/task_lists_spec.rb19
-rw-r--r--spec/features/triggers_spec.rb4
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb1
-rw-r--r--spec/features/users_spec.rb4
-rw-r--r--spec/finders/admin/projects_finder_spec.rb12
-rw-r--r--spec/finders/contributed_projects_finder_spec.rb4
-rw-r--r--spec/finders/environments_finder_spec.rb2
-rw-r--r--spec/finders/group_members_finder_spec.rb4
-rw-r--r--spec/finders/issues_finder_spec.rb53
-rw-r--r--spec/finders/members_finder_spec.rb2
-rw-r--r--spec/finders/merge_requests_finder_spec.rb14
-rw-r--r--spec/finders/projects_finder_spec.rb6
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request.json2
-rw-r--r--spec/fixtures/api/schemas/issue.json6
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedule.json4
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedule_variable.json8
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issues.json8
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_requests.json8
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/login.json1
-rw-r--r--spec/fixtures/emails/ios_default.eml12
-rw-r--r--spec/fixtures/emails/on_wrote.eml6
-rw-r--r--spec/fixtures/encoding/Japanese.md42
-rw-r--r--spec/fixtures/fuzzy.po27
-rw-r--r--spec/fixtures/invalid.po25
-rw-r--r--spec/fixtures/missing_metadata.po4
-rw-r--r--spec/fixtures/missing_plurals.po22
-rw-r--r--spec/fixtures/multiple_plurals.po26
-rw-r--r--spec/fixtures/newlines.po48
-rw-r--r--spec/fixtures/unescaped_chars.po21
-rw-r--r--spec/fixtures/valid.po1136
-rw-r--r--spec/helpers/auto_devops_helper_spec.rb59
-rw-r--r--spec/helpers/avatars_helper_spec.rb42
-rw-r--r--spec/helpers/blame_helper_spec.rb27
-rw-r--r--spec/helpers/blob_helper_spec.rb4
-rw-r--r--spec/helpers/button_helper_spec.rb63
-rw-r--r--spec/helpers/diff_helper_spec.rb8
-rw-r--r--spec/helpers/events_helper_spec.rb10
-rw-r--r--spec/helpers/groups_helper_spec.rb96
-rw-r--r--spec/helpers/issuables_helper_spec.rb106
-rw-r--r--spec/helpers/issues_helper_spec.rb10
-rw-r--r--spec/helpers/markup_helper_spec.rb91
-rw-r--r--spec/helpers/notes_helper_spec.rb32
-rw-r--r--spec/helpers/pagination_helper_spec.rb23
-rw-r--r--spec/helpers/preferences_helper_spec.rb30
-rw-r--r--spec/helpers/profiles_helper_spec.rb31
-rw-r--r--spec/helpers/projects_helper_spec.rb41
-rw-r--r--spec/helpers/search_helper_spec.rb20
-rw-r--r--spec/helpers/tree_helper_spec.rb26
-rw-r--r--spec/helpers/version_check_helper_spec.rb6
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb75
-rw-r--r--spec/initializers/settings_spec.rb25
-rw-r--r--spec/javascripts/api_spec.js30
-rw-r--r--spec/javascripts/awards_handler_spec.js7
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js190
-rw-r--r--spec/javascripts/blob/blob_file_dropzone_spec.js42
-rw-r--r--spec/javascripts/boards/board_blank_state_spec.js3
-rw-r--r--spec/javascripts/boards/board_card_spec.js5
-rw-r--r--spec/javascripts/boards/board_list_spec.js4
-rw-r--r--spec/javascripts/boards/board_new_issue_spec.js11
-rw-r--r--spec/javascripts/boards/boards_store_spec.js6
-rw-r--r--spec/javascripts/boards/components/board_spec.js10
-rw-r--r--spec/javascripts/boards/issue_card_spec.js80
-rw-r--r--spec/javascripts/boards/issue_spec.js4
-rw-r--r--spec/javascripts/boards/list_spec.js15
-rw-r--r--spec/javascripts/boards/mock_data.js22
-rw-r--r--spec/javascripts/boards/modal_store_spec.js2
-rw-r--r--spec/javascripts/breakpoints_spec.js15
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js4
-rw-r--r--spec/javascripts/droplab/drop_down_spec.js15
-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/fixtures/abuse_reports.rb2
-rw-r--r--spec/javascripts/fixtures/blob.rb33
-rw-r--r--spec/javascripts/fixtures/boards.rb2
-rw-r--r--spec/javascripts/fixtures/branches.rb6
-rw-r--r--spec/javascripts/fixtures/dashboard.rb6
-rw-r--r--spec/javascripts/fixtures/deploy_keys.rb6
-rw-r--r--spec/javascripts/fixtures/environments.rb2
-rw-r--r--spec/javascripts/fixtures/issues.rb6
-rw-r--r--spec/javascripts/fixtures/jobs.rb6
-rw-r--r--spec/javascripts/fixtures/labels.rb8
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb11
-rw-r--r--spec/javascripts/fixtures/merge_requests_diffs.rb6
-rw-r--r--spec/javascripts/fixtures/pipelines.rb2
-rw-r--r--spec/javascripts/fixtures/project_select_combo_button.html.haml2
-rw-r--r--spec/javascripts/fixtures/projects.rb6
-rw-r--r--spec/javascripts/fixtures/prometheus_service.rb8
-rw-r--r--spec/javascripts/fixtures/raw.rb4
-rw-r--r--spec/javascripts/fixtures/services.rb7
-rw-r--r--spec/javascripts/fixtures/snippet.rb6
-rw-r--r--spec/javascripts/fixtures/todos.rb8
-rw-r--r--spec/javascripts/fly_out_nav_spec.js252
-rw-r--r--spec/javascripts/gl_dropdown_spec.js325
-rw-r--r--spec/javascripts/gpg_badges_spec.js48
-rw-r--r--spec/javascripts/groups/group_identicon_spec.js60
-rw-r--r--spec/javascripts/helpers/vue_mount_component_helper.js4
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js25
-rw-r--r--spec/javascripts/issue_show/components/edited_spec.js10
-rw-r--r--spec/javascripts/issue_show/components/fields/description_spec.js4
-rw-r--r--spec/javascripts/issue_show/components/fields/project_move_spec.js38
-rw-r--r--spec/javascripts/issue_show/components/form_spec.js6
-rw-r--r--spec/javascripts/issue_spec.js2
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js52
-rw-r--r--spec/javascripts/lib/utils/text_utility_spec.js1
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js11
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js49
-rw-r--r--spec/javascripts/monitoring/dashboard_state_spec.js110
-rw-r--r--spec/javascripts/monitoring/graph/deployment_spec.js137
-rw-r--r--spec/javascripts/monitoring/graph/flag_spec.js72
-rw-r--r--spec/javascripts/monitoring/graph/legend_spec.js107
-rw-r--r--spec/javascripts/monitoring/graph_spec.js89
-rw-r--r--spec/javascripts/monitoring/mock_data.js7580
-rw-r--r--spec/javascripts/monitoring/monitoring_column_spec.js109
-rw-r--r--spec/javascripts/monitoring/monitoring_deployment_spec.js137
-rw-r--r--spec/javascripts/monitoring/monitoring_flag_spec.js76
-rw-r--r--spec/javascripts/monitoring/monitoring_legends_spec.js111
-rw-r--r--spec/javascripts/monitoring/monitoring_paths_spec.js34
-rw-r--r--spec/javascripts/monitoring/monitoring_row_spec.js57
-rw-r--r--spec/javascripts/monitoring/monitoring_spec.js49
-rw-r--r--spec/javascripts/monitoring/monitoring_state_spec.js110
-rw-r--r--spec/javascripts/monitoring/monitoring_store_spec.js4
-rw-r--r--spec/javascripts/monitoring/utils/multiple_time_series_spec.js21
-rw-r--r--spec/javascripts/notes/components/issue_comment_form_spec.js134
-rw-r--r--spec/javascripts/notes/components/issue_discussion_spec.js50
-rw-r--r--spec/javascripts/notes/components/issue_note_actions_spec.js91
-rw-r--r--spec/javascripts/notes/components/issue_note_app_spec.js255
-rw-r--r--spec/javascripts/notes/components/issue_note_attachment_spec.js23
-rw-r--r--spec/javascripts/notes/components/issue_note_awards_list_spec.js56
-rw-r--r--spec/javascripts/notes/components/issue_note_body_spec.js46
-rw-r--r--spec/javascripts/notes/components/issue_note_edited_text_spec.js47
-rw-r--r--spec/javascripts/notes/components/issue_note_form_spec.js112
-rw-r--r--spec/javascripts/notes/components/issue_note_header_spec.js94
-rw-r--r--spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js37
-rw-r--r--spec/javascripts/notes/components/issue_note_spec.js44
-rw-r--r--spec/javascripts/notes/components/issue_placeholder_note_spec.js39
-rw-r--r--spec/javascripts/notes/components/issue_placeholder_system_note_spec.js24
-rw-r--r--spec/javascripts/notes/components/issue_system_note_spec.js53
-rw-r--r--spec/javascripts/notes/mock_data.js449
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js62
-rw-r--r--spec/javascripts/notes/stores/getters_spec.js58
-rw-r--r--spec/javascripts/notes/stores/helpers.js37
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js207
-rw-r--r--spec/javascripts/notes_spec.js14
-rw-r--r--spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js2
-rw-r--r--spec/javascripts/pipelines/navigation_tabs_spec.js127
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js27
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js2
-rw-r--r--spec/javascripts/pipelines/pipelines_table_spec.js3
-rw-r--r--spec/javascripts/pretty_time_spec.js81
-rw-r--r--spec/javascripts/project_select_combo_button_spec.js50
-rw-r--r--spec/javascripts/project_title_spec.js57
-rw-r--r--spec/javascripts/projects_dropdown/components/app_spec.js348
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js72
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_item_spec.js65
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_search_spec.js84
-rw-r--r--spec/javascripts/projects_dropdown/components/search_spec.js101
-rw-r--r--spec/javascripts/projects_dropdown/mock_data.js96
-rw-r--r--spec/javascripts/projects_dropdown/service/projects_service_spec.js179
-rw-r--r--spec/javascripts/projects_dropdown/store/projects_store_spec.js41
-rw-r--r--spec/javascripts/repo/components/repo_commit_section_spec.js67
-rw-r--r--spec/javascripts/repo/components/repo_edit_button_spec.js18
-rw-r--r--spec/javascripts/repo/components/repo_editor_spec.js51
-rw-r--r--spec/javascripts/repo/components/repo_file_buttons_spec.js19
-rw-r--r--spec/javascripts/repo/components/repo_file_spec.js6
-rw-r--r--spec/javascripts/repo/components/repo_loading_file_spec.js2
-rw-r--r--spec/javascripts/repo/components/repo_sidebar_spec.js50
-rw-r--r--spec/javascripts/repo/components/repo_tab_spec.js34
-rw-r--r--spec/javascripts/repo/components/repo_tabs_spec.js23
-rw-r--r--spec/javascripts/repo/monaco_loader_spec.js12
-rw-r--r--spec/javascripts/repo/services/repo_service_spec.js50
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js124
-rw-r--r--spec/javascripts/shortcuts_spec.js2
-rw-r--r--spec/javascripts/sidebar/confidential_issue_sidebar_spec.js2
-rw-r--r--spec/javascripts/sidebar/mock_data.js41
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js40
-rw-r--r--spec/javascripts/sidebar/sidebar_move_issue_spec.js142
-rw-r--r--spec/javascripts/sidebar/sidebar_service_spec.js28
-rw-r--r--spec/javascripts/sidebar/sidebar_store_spec.js14
-rw-r--r--spec/javascripts/user_callout_spec.js13
-rw-r--r--spec/javascripts/vue_shared/components/identicon_spec.js72
-rw-r--r--spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js20
-rw-r--r--spec/javascripts/vue_shared/components/markdown/field_spec.js7
-rw-r--r--spec/javascripts/zen_mode_spec.js2
-rw-r--r--spec/lib/after_commit_queue_spec.rb15
-rw-r--r--spec/lib/api/helpers/pagination_spec.rb52
-rw-r--r--spec/lib/banzai/commit_renderer_spec.rb19
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb36
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb90
-rw-r--r--spec/lib/banzai/renderer_spec.rb36
-rw-r--r--spec/lib/bitbucket/paginator_spec.rb2
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb85
-rw-r--r--spec/lib/container_registry/tag_spec.rb2
-rw-r--r--spec/lib/event_filter_spec.rb2
-rw-r--r--spec/lib/extracts_path_spec.rb2
-rw-r--r--spec/lib/file_size_validator_spec.rb4
-rw-r--r--spec/lib/gitlab/auth/unique_ips_limiter_spec.rb2
-rw-r--r--spec/lib/gitlab/auth_spec.rb10
-rw-r--r--spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb26
-rw-r--r--spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb420
-rw-r--r--spec/lib/gitlab/background_migration/migrate_stage_status_spec.rb80
-rw-r--r--spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb72
-rw-r--r--spec/lib/gitlab/bare_repository_importer_spec.rb100
-rw-r--r--spec/lib/gitlab/bitbucket_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/checks/force_push_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/path_spec.rb64
-rw-r--r--spec/lib/gitlab/ci/config/entry/attributable_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/config/entry/configurable_spec.rb36
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb117
-rw-r--r--spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb88
-rw-r--r--spec/lib/gitlab/ci/config/entry/trigger_spec.rb56
-rw-r--r--spec/lib/gitlab/ci/config/entry/validatable_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/entry/validator_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/stage/seed_spec.rb20
-rw-r--r--spec/lib/gitlab/ci/trace/stream_spec.rb2
-rw-r--r--spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb4
-rw-r--r--spec/lib/gitlab/daemon_spec.rb2
-rw-r--r--spec/lib/gitlab/data_builder/note_spec.rb2
-rw-r--r--spec/lib/gitlab/database/grant_spec.rb30
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb83
-rw-r--r--spec/lib/gitlab/database_spec.rb22
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb58
-rw-r--r--spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb6
-rw-r--r--spec/lib/gitlab/diff/inline_diff_marker_spec.rb14
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/email/message/repository_push_spec.rb4
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb47
-rw-r--r--spec/lib/gitlab/exclusive_lease_spec.rb12
-rw-r--r--spec/lib/gitlab/file_finder_spec.rb14
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb2
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb2
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb69
-rw-r--r--spec/lib/gitlab/git/committer_spec.rb22
-rw-r--r--spec/lib/gitlab/git/compare_spec.rb2
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb6
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb21
-rw-r--r--spec/lib/gitlab/git/hook_spec.rb9
-rw-r--r--spec/lib/gitlab/git/hooks_service_spec.rb48
-rw-r--r--spec/lib/gitlab/git/index_spec.rb2
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb333
-rw-r--r--spec/lib/gitlab/git/storage/circuit_breaker_spec.rb67
-rw-r--r--spec/lib/gitlab/git/storage/forked_storage_check_spec.rb2
-rw-r--r--spec/lib/gitlab/git/tag_spec.rb2
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb5
-rw-r--r--spec/lib/gitlab/git_access_spec.rb289
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb67
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb21
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb67
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb16
-rw-r--r--spec/lib/gitlab/gitlab_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/google_code_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb253
-rw-r--r--spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb44
-rw-r--r--spec/lib/gitlab/gpg_spec.rb67
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb6
-rw-r--r--spec/lib/gitlab/i18n/metadata_entry_spec.rb51
-rw-r--r--spec/lib/gitlab/i18n/po_linter_spec.rb338
-rw-r--r--spec/lib/gitlab/i18n/translation_entry_spec.rb203
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml5
-rw-r--r--spec/lib/gitlab/import_export/attribute_cleaner_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/file_importer_spec.rb5
-rw-r--r--spec/lib/gitlab/import_export/project.json92
-rw-r--r--spec/lib/gitlab/import_export/project.light.json100
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb55
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb29
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml26
-rw-r--r--spec/lib/gitlab/issuables_count_for_state_spec.rb37
-rw-r--r--spec/lib/gitlab/job_waiter_spec.rb41
-rw-r--r--spec/lib/gitlab/key_fingerprint_spec.rb82
-rw-r--r--spec/lib/gitlab/ldap/adapter_spec.rb6
-rw-r--r--spec/lib/gitlab/ldap/auth_hash_spec.rb12
-rw-r--r--spec/lib/gitlab/ldap/config_spec.rb4
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb29
-rw-r--r--spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb18
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb196
-rw-r--r--spec/lib/gitlab/project_template_spec.rb4
-rw-r--r--spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb4
-rw-r--r--spec/lib/gitlab/redis/wrapper_spec.rb7
-rw-r--r--spec/lib/gitlab/reference_counter_spec.rb37
-rw-r--r--spec/lib/gitlab/request_context_spec.rb2
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb4
-rw-r--r--spec/lib/gitlab/sentry_spec.rb13
-rw-r--r--spec/lib/gitlab/shell_spec.rb99
-rw-r--r--spec/lib/gitlab/sidekiq_throttler_spec.rb50
-rw-r--r--spec/lib/gitlab/sql/pattern_spec.rb175
-rw-r--r--spec/lib/gitlab/sql/union_spec.rb2
-rw-r--r--spec/lib/gitlab/ssh_public_key_spec.rb136
-rw-r--r--spec/lib/gitlab/string_range_marker_spec.rb39
-rw-r--r--spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb18
-rw-r--r--spec/lib/gitlab/template/issue_template_spec.rb44
-rw-r--r--spec/lib/gitlab/template/merge_request_template_spec.rb44
-rw-r--r--spec/lib/gitlab/themes_spec.rb48
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb34
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb179
-rw-r--r--spec/lib/gitlab/utils_spec.rb22
-rw-r--r--spec/lib/gitlab/version_info_spec.rb4
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb22
-rw-r--r--spec/lib/json_web_token/rsa_token_spec.rb4
-rw-r--r--spec/lib/rspec_flaky/example_spec.rb89
-rw-r--r--spec/lib/rspec_flaky/flaky_example_spec.rb104
-rw-r--r--spec/lib/rspec_flaky/listener_spec.rb178
-rw-r--r--spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb79
-rw-r--r--spec/lib/system_check/simple_executor_spec.rb26
-rw-r--r--spec/mailers/notify_spec.rb101
-rw-r--r--spec/migrations/README.md10
-rw-r--r--spec/migrations/clean_upload_symlinks_spec.rb2
-rw-r--r--spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb2
-rw-r--r--spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb32
-rw-r--r--spec/migrations/convert_custom_notification_settings_to_columns_spec.rb6
-rw-r--r--spec/migrations/migrate_issues_to_ghost_user_spec.rb51
-rw-r--r--spec/migrations/migrate_old_artifacts_spec.rb4
-rw-r--r--spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb55
-rw-r--r--spec/migrations/migrate_process_commit_worker_jobs_spec.rb2
-rw-r--r--spec/migrations/migrate_stage_id_reference_in_background_spec.rb13
-rw-r--r--spec/migrations/migrate_stages_statuses_spec.rb68
-rw-r--r--spec/migrations/move_personal_snippets_files_spec.rb10
-rw-r--r--spec/migrations/move_system_upload_folder_spec.rb18
-rw-r--r--spec/migrations/move_uploads_to_system_dir_spec.rb2
-rw-r--r--spec/migrations/remove_dot_git_from_usernames_spec.rb3
-rw-r--r--spec/migrations/remove_duplicate_mr_events_spec.rb26
-rw-r--r--spec/migrations/rename_system_namespaces_spec.rb254
-rw-r--r--spec/migrations/update_upload_paths_to_system_spec.rb8
-rw-r--r--spec/models/appearance_spec.rb35
-rw-r--r--spec/models/application_setting_spec.rb60
-rw-r--r--spec/models/award_emoji_spec.rb36
-rw-r--r--spec/models/broadcast_message_spec.rb43
-rw-r--r--spec/models/ci/build_spec.rb98
-rw-r--r--spec/models/ci/pipeline_spec.rb144
-rw-r--r--spec/models/ci/runner_spec.rb76
-rw-r--r--spec/models/ci/stage_spec.rb90
-rw-r--r--spec/models/ci/trigger_request_spec.rb17
-rw-r--r--spec/models/commit_spec.rb65
-rw-r--r--spec/models/commit_status_spec.rb29
-rw-r--r--spec/models/concerns/awardable_spec.rb22
-rw-r--r--spec/models/concerns/editable_spec.rb6
-rw-r--r--spec/models/concerns/issuable_spec.rb113
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb2
-rw-r--r--spec/models/concerns/resolvable_note_spec.rb16
-rw-r--r--spec/models/container_repository_spec.rb2
-rw-r--r--spec/models/event_collection_spec.rb51
-rw-r--r--spec/models/event_spec.rb77
-rw-r--r--spec/models/gpg_key_spec.rb38
-rw-r--r--spec/models/group_spec.rb96
-rw-r--r--spec/models/issue_spec.rb48
-rw-r--r--spec/models/key_spec.rb55
-rw-r--r--spec/models/member_spec.rb9
-rw-r--r--spec/models/members/project_member_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb108
-rw-r--r--spec/models/namespace_spec.rb144
-rw-r--r--spec/models/project_auto_devops_spec.rb23
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb2
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb2
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb16
-rw-r--r--spec/models/project_spec.rb547
-rw-r--r--spec/models/protectable_dropdown_spec.rb7
-rw-r--r--spec/models/push_event_payload_spec.rb16
-rw-r--r--spec/models/push_event_spec.rb202
-rw-r--r--spec/models/redirect_route_spec.rb12
-rw-r--r--spec/models/repository_spec.rb140
-rw-r--r--spec/models/route_spec.rb74
-rw-r--r--spec/models/user_spec.rb200
-rw-r--r--spec/models/wiki_page_spec.rb6
-rw-r--r--spec/policies/group_policy_spec.rb130
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb34
-rw-r--r--spec/requests/api/award_emoji_spec.rb16
-rw-r--r--spec/requests/api/boards_spec.rb4
-rw-r--r--spec/requests/api/branches_spec.rb20
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb4
-rw-r--r--spec/requests/api/commit_statuses_spec.rb11
-rw-r--r--spec/requests/api/commits_spec.rb223
-rw-r--r--spec/requests/api/deploy_keys_spec.rb6
-rw-r--r--spec/requests/api/environments_spec.rb4
-rw-r--r--spec/requests/api/events_spec.rb49
-rw-r--r--spec/requests/api/files_spec.rb23
-rw-r--r--spec/requests/api/group_variables_spec.rb12
-rw-r--r--spec/requests/api/groups_spec.rb52
-rw-r--r--spec/requests/api/helpers_spec.rb4
-rw-r--r--spec/requests/api/internal_spec.rb125
-rw-r--r--spec/requests/api/issues_spec.rb30
-rw-r--r--spec/requests/api/jobs_spec.rb89
-rw-r--r--spec/requests/api/labels_spec.rb5
-rw-r--r--spec/requests/api/members_spec.rb4
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb224
-rw-r--r--spec/requests/api/notes_spec.rb12
-rw-r--r--spec/requests/api/pipeline_schedules_spec.rb169
-rw-r--r--spec/requests/api/project_hooks_spec.rb4
-rw-r--r--spec/requests/api/project_snippets_spec.rb11
-rw-r--r--spec/requests/api/projects_spec.rb145
-rw-r--r--spec/requests/api/protected_branches_spec.rb8
-rw-r--r--spec/requests/api/runner_spec.rb65
-rw-r--r--spec/requests/api/runners_spec.rb46
-rw-r--r--spec/requests/api/settings_spec.rb17
-rw-r--r--spec/requests/api/snippets_spec.rb12
-rw-r--r--spec/requests/api/system_hooks_spec.rb4
-rw-r--r--spec/requests/api/tags_spec.rb6
-rw-r--r--spec/requests/api/triggers_spec.rb31
-rw-r--r--spec/requests/api/users_spec.rb360
-rw-r--r--spec/requests/api/v3/commits_spec.rb6
-rw-r--r--spec/requests/api/v3/deploy_keys_spec.rb4
-rw-r--r--spec/requests/api/v3/files_spec.rb4
-rw-r--r--spec/requests/api/v3/groups_spec.rb2
-rw-r--r--spec/requests/api/v3/issues_spec.rb4
-rw-r--r--spec/requests/api/v3/merge_request_diffs_spec.rb2
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb32
-rw-r--r--spec/requests/api/v3/project_snippets_spec.rb6
-rw-r--r--spec/requests/api/v3/projects_spec.rb9
-rw-r--r--spec/requests/api/v3/runners_spec.rb12
-rw-r--r--spec/requests/api/v3/snippets_spec.rb6
-rw-r--r--spec/requests/api/v3/triggers_spec.rb14
-rw-r--r--spec/requests/api/v3/users_spec.rb25
-rw-r--r--spec/requests/api/variables_spec.rb8
-rw-r--r--spec/requests/api/wikis_spec.rb679
-rw-r--r--spec/requests/ci/api/builds_spec.rb923
-rw-r--r--spec/requests/ci/api/runners_spec.rb127
-rw-r--r--spec/requests/ci/api/triggers_spec.rb90
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/safer_boolean_column_spec.rb85
-rw-r--r--spec/serializers/analytics_build_entity_spec.rb14
-rw-r--r--spec/serializers/note_entity_spec.rb51
-rw-r--r--spec/serializers/pipeline_entity_spec.rb2
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb2
-rw-r--r--spec/services/boards/issues/create_service_spec.rb2
-rw-r--r--spec/services/boards/issues/move_service_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb105
-rw-r--r--spec/services/ci/create_trigger_request_service_spec.rb52
-rw-r--r--spec/services/ci/register_job_service_spec.rb92
-rw-r--r--spec/services/ci/retry_build_service_spec.rb15
-rw-r--r--spec/services/create_deployment_service_spec.rb4
-rw-r--r--spec/services/discussions/update_diff_position_service_spec.rb62
-rw-r--r--spec/services/event_create_service_spec.rb44
-rw-r--r--spec/services/files/update_service_spec.rb2
-rw-r--r--spec/services/git_hooks_service_spec.rb50
-rw-r--r--spec/services/git_push_service_spec.rb47
-rw-r--r--spec/services/git_tag_push_service_spec.rb2
-rw-r--r--spec/services/groups/create_service_spec.rb57
-rw-r--r--spec/services/groups/destroy_service_spec.rb26
-rw-r--r--spec/services/groups/nested_create_service_spec.rb88
-rw-r--r--spec/services/groups/update_service_spec.rb34
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb2
-rw-r--r--spec/services/issues/close_service_spec.rb5
-rw-r--r--spec/services/issues/create_service_spec.rb14
-rw-r--r--spec/services/issues/reopen_service_spec.rb7
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb24
-rw-r--r--spec/services/labels/update_service_spec.rb2
-rw-r--r--spec/services/merge_requests/close_service_spec.rb7
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb20
-rw-r--r--spec/services/merge_requests/create_service_spec.rb38
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb8
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb34
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb7
-rw-r--r--spec/services/merge_requests/update_service_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb131
-rw-r--r--spec/services/projects/after_import_service_spec.rb55
-rw-r--r--spec/services/projects/count_service_spec.rb73
-rw-r--r--spec/services/projects/create_service_spec.rb42
-rw-r--r--spec/services/projects/destroy_service_spec.rb4
-rw-r--r--spec/services/projects/fork_service_spec.rb30
-rw-r--r--spec/services/projects/forks_count_service_spec.rb14
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb16
-rw-r--r--spec/services/projects/open_issues_count_service_spec.rb21
-rw-r--r--spec/services/projects/open_merge_requests_count_service_spec.rb15
-rw-r--r--spec/services/projects/transfer_service_spec.rb20
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb10
-rw-r--r--spec/services/projects/update_pages_service_spec.rb1
-rw-r--r--spec/services/projects/update_service_spec.rb30
-rw-r--r--spec/services/protected_branches/update_service_spec.rb2
-rw-r--r--spec/services/protected_tags/update_service_spec.rb2
-rw-r--r--spec/services/push_event_payload_service_spec.rb218
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb10
-rw-r--r--spec/services/spam_service_spec.rb6
-rw-r--r--spec/services/system_note_service_spec.rb61
-rw-r--r--spec/services/tags/create_service_spec.rb2
-rw-r--r--spec/services/test_hooks/project_service_spec.rb27
-rw-r--r--spec/services/test_hooks/system_service_spec.rb36
-rw-r--r--spec/services/todo_service_spec.rb4
-rw-r--r--spec/services/user_project_access_changed_service_spec.rb7
-rw-r--r--spec/services/users/destroy_service_spec.rb27
-rw-r--r--spec/services/users/update_service_spec.rb32
-rw-r--r--spec/services/web_hook_service_spec.rb6
-rw-r--r--spec/simplecov_env.rb25
-rw-r--r--spec/spec_helper.rb33
-rw-r--r--spec/support/api/milestones_shared_examples.rb2
-rw-r--r--spec/support/api_helpers.rb14
-rw-r--r--spec/support/background_migrations_matchers.rb13
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb14
-rw-r--r--spec/support/cycle_analytics_helpers.rb8
-rw-r--r--spec/support/db_cleaner.rb8
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb27
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb24
-rw-r--r--spec/support/features/reportable_note_shared_examples.rb18
-rw-r--r--spec/support/filtered_search_helpers.rb36
-rwxr-xr-xspec/support/generate-seed-repo-rb12
-rw-r--r--spec/support/gitlab-git-test.git/objects/3e/20715310a699808282e772720b9c04a0696bccbin0 -> 566 bytes
-rw-r--r--spec/support/gitlab-git-test.git/objects/95/96bc54a6f0c0c98248fe97077eb5ccf48a98d02
-rw-r--r--spec/support/gitlab-git-test.git/packed-refs1
-rw-r--r--spec/support/gitlab_stubs/session.json2
-rw-r--r--spec/support/gitlab_stubs/user.json4
-rw-r--r--spec/support/gpg_helpers.rb2
-rw-r--r--spec/support/group_members_shared_example.rb27
-rw-r--r--spec/support/helpers/note_interaction_helpers.rb2
-rw-r--r--spec/support/import_export/export_file_helper.rb3
-rw-r--r--spec/support/issuables_list_metadata_shared_examples.rb4
-rw-r--r--spec/support/javascript_fixtures_helpers.rb4
-rw-r--r--spec/support/matchers/access_matchers_for_controller.rb2
-rw-r--r--spec/support/matchers/query_matcher.rb2
-rw-r--r--spec/support/migrations_helpers.rb31
-rw-r--r--spec/support/notify_shared_examples.rb5
-rw-r--r--spec/support/rake_helpers.rb10
-rw-r--r--spec/support/redis/redis_shared_examples.rb4
-rw-r--r--spec/support/seed_helper.rb2
-rw-r--r--spec/support/seed_repo.rb1
-rw-r--r--spec/support/shared_examples/requests/api/status_shared_examples.rb25
-rw-r--r--spec/support/stub_configuration.rb14
-rw-r--r--spec/support/stub_env.rb2
-rw-r--r--spec/support/test_env.rb28
-rw-r--r--spec/support/unique_ip_check_shared_examples.rb2
-rw-r--r--spec/tasks/config_lint_spec.rb8
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb4
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb31
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb3
-rw-r--r--spec/tasks/gitlab/workhorse_rake_spec.rb16
-rw-r--r--spec/uploaders/file_mover_spec.rb14
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb4
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb1
-rw-r--r--spec/views/ci/lints/show.html.haml_spec.rb4
-rw-r--r--spec/views/devise/shared/_signin_box.html.haml_spec.rb1
-rw-r--r--spec/views/groups/edit.html.haml_spec.rb116
-rw-r--r--spec/views/help/index.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb4
-rw-r--r--spec/views/layouts/nav/_project.html.haml_spec.rb37
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb39
-rw-r--r--spec/views/projects/commits/_commit.html.haml_spec.rb26
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb15
-rw-r--r--spec/views/projects/jobs/show.html.haml_spec.rb16
-rw-r--r--spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb1
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb4
-rw-r--r--spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb6
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb1
-rw-r--r--spec/views/shared/projects/_project.html.haml_spec.rb4
-rw-r--r--spec/workers/authorized_projects_worker_spec.rb71
-rw-r--r--spec/workers/build_finished_worker_spec.rb2
-rw-r--r--spec/workers/concerns/build_queue_spec.rb14
-rw-r--r--spec/workers/concerns/pipeline_queue_spec.rb14
-rw-r--r--spec/workers/create_gpg_signature_worker_spec.rb25
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb123
-rw-r--r--spec/workers/merge_worker_spec.rb11
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb16
-rw-r--r--spec/workers/new_issue_worker_spec.rb2
-rw-r--r--spec/workers/new_merge_request_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_metrics_worker_spec.rb9
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb2
-rw-r--r--spec/workers/post_receive_spec.rb6
-rw-r--r--spec/workers/prune_old_events_worker_spec.rb8
-rw-r--r--spec/workers/repository_import_worker_spec.rb2
-rw-r--r--spec/workers/stage_update_worker_spec.rb22
-rw-r--r--spec/workers/stuck_ci_jobs_worker_spec.rb22
-rw-r--r--spec/workers/stuck_import_jobs_worker_spec.rb34
-rw-r--r--vendor/Dockerfile/CONTRIBUTING.md47
-rw-r--r--vendor/assets/stylesheets/katex.scss34
-rw-r--r--vendor/assets/stylesheets/xterm/xterm.css2
-rw-r--r--vendor/gitignore/Actionscript.gitignore5
-rw-r--r--vendor/gitignore/Drupal.gitignore3
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore2
-rw-r--r--vendor/gitignore/Haskell.gitignore1
l---------vendor/gitignore/Kotlin.gitignore1
-rw-r--r--vendor/gitignore/Nanoc.gitignore2
-rw-r--r--vendor/gitignore/Node.gitignore2
-rw-r--r--vendor/gitignore/Prestashop.gitignore4
-rw-r--r--vendor/gitignore/Qt.gitignore2
-rw-r--r--vendor/gitignore/Smalltalk.gitignore4
-rw-r--r--vendor/gitignore/Swift.gitignore1
-rw-r--r--vendor/gitignore/Symfony.gitignore3
-rw-r--r--vendor/gitignore/Terraform.gitignore1
-rw-r--r--vendor/gitignore/VisualStudio.gitignore6
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml411
-rw-r--r--vendor/gitlab-ci-yml/Bash.gitlab-ci.yml34
-rw-r--r--vendor/gitlab-ci-yml/Go.gitlab-ci.yml32
-rw-r--r--vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml43
-rw-r--r--vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml4
-rw-r--r--vendor/gitlab-ci-yml/PHP.gitlab-ci.yml3
-rw-r--r--vendor/gitlab-ci-yml/Packer.gitlab-ci.yml26
-rw-r--r--vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml8
-rw-r--r--vendor/gitlab-ci-yml/Terraform.gitlab-ci.yml55
-rw-r--r--vendor/project_templates/express.tar.gzbin0 -> 5648 bytes
-rw-r--r--vendor/project_templates/rails.tar.gzbin899958 -> 25063 bytes
-rw-r--r--vendor/project_templates/spring.tar.gzbin0 -> 50838 bytes
-rw-r--r--yarn.lock898
2628 files changed, 67894 insertions, 25503 deletions
diff --git a/.eslintrc b/.eslintrc
index 3e07edbccfe..44ad6a4896c 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -6,6 +6,7 @@
},
"extends": "airbnb-base",
"globals": {
+ "__webpack_public_path__": true,
"_": false,
"gl": false,
"gon": false,
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f55bdc19eec..dadce073309 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-phantomjs-2.1-node-7.1-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-7.1-postgresql-9.6"
.default-cache: &default-cache
key: "ruby-233-with-yarn"
@@ -27,6 +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
before_script:
- bundle --version
@@ -39,22 +40,25 @@ stages:
- test
- post-test
- pages
+ - post-cleanup
# Predefined scopes
.dedicated-runner: &dedicated-runner
+ retry: 1
tags:
- gitlab-org
-.knapsack-state: &knapsack-state
+.tests-metadata-state: &tests-metadata-state
services: []
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
- KNAPSACK_S3_BUCKET: "gitlab-ce-cache"
+ TESTS_METADATA_S3_BUCKET: "gitlab-ce-cache"
artifacts:
expire_in: 31d
paths:
- knapsack/
+ - rspec_flaky/
.use-pg: &use-pg
services:
@@ -66,19 +70,6 @@ stages:
- mysql:latest
- redis:alpine
-.only-if-want-mysql: &only-if-want-mysql
- only:
- - /mysql/
- - /-stable/
- - master@gitlab-org/gitlab-ce
- - master@gitlab-org/gitlab-ee
- - master@gitlab/gitlabhq
- - master@gitlab/gitlab-ee
- - tags@gitlab-org/gitlab-ce
- - tags@gitlab-org/gitlab-ee
- - tags@gitlab/gitlabhq
- - tags@gitlab/gitlab-ee
-
# Skip all jobs except the ones that begin with 'docs/'.
# Used for commits including ONLY documentation changes.
# https://docs.gitlab.com/ce/development/writing_documentation.html#testing
@@ -86,7 +77,7 @@ stages:
except:
- /(^docs[\/-].*|.*-docs$)/
-.rspec-knapsack: &rspec-knapsack
+.rspec-metadata: &rspec-metadata
<<: *dedicated-runner
<<: *pull-cache
stage: test
@@ -96,8 +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 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 $NEW_FLAKY_RSPEC_REPORT_PATH ]] || echo "{}" > ${NEW_FLAKY_RSPEC_REPORT_PATH}'
- scripts/gitaly-test-spawn
- knapsack rspec "--color --format documentation"
artifacts:
@@ -106,20 +102,20 @@ stages:
paths:
- coverage/
- knapsack/
+ - rspec_flaky/
- tmp/capybara/
-.rspec-knapsack-pg: &rspec-knapsack-pg
- <<: *rspec-knapsack
+.rspec-metadata-pg: &rspec-metadata-pg
+ <<: *rspec-metadata
<<: *use-pg
<<: *except-docs
-.rspec-knapsack-mysql: &rspec-knapsack-mysql
- <<: *rspec-knapsack
+.rspec-metadata-mysql: &rspec-metadata-mysql
+ <<: *rspec-metadata
<<: *use-mysql
- <<: *only-if-want-mysql
<<: *except-docs
-.spinach-knapsack: &spinach-knapsack
+.spinach-metadata: &spinach-metadata
<<: *dedicated-runner
<<: *pull-cache
stage: test
@@ -131,6 +127,7 @@ stages:
- export KNAPSACK_GENERATE_REPORT=true
- 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)'
artifacts:
expire_in: 31d
@@ -140,15 +137,14 @@ stages:
- knapsack/
- tmp/capybara/
-.spinach-knapsack-pg: &spinach-knapsack-pg
- <<: *spinach-knapsack
+.spinach-metadata-pg: &spinach-metadata-pg
+ <<: *spinach-metadata
<<: *use-pg
<<: *except-docs
-.spinach-knapsack-mysql: &spinach-knapsack-mysql
- <<: *spinach-knapsack
+.spinach-metadata-mysql: &spinach-metadata-mysql
+ <<: *spinach-metadata
<<: *use-mysql
- <<: *only-if-want-mysql
<<: *except-docs
.only-canonical-masters: &only-canonical-masters
@@ -158,8 +154,7 @@ stages:
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
-# Trigger a package build on omnibus-gitlab repository
-
+# Trigger a package build in omnibus-gitlab repository
build-package:
image: ruby:2.3-alpine
before_script: []
@@ -171,45 +166,112 @@ build-package:
cache: {}
when: manual
script:
- - scripts/trigger-build
+ - scripts/trigger-build-omnibus
only:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
-# Prepare and merge knapsack tests
-knapsack:
- <<: *knapsack-state
+# Review docs base
+.review-docs: &review-docs
+ image: ruby:2.4-alpine
+ before_script: []
+ services: []
+ variables:
+ SETUP_DB: "false"
+ USE_BUNDLE_INSTALL: "false"
+ cache: {}
+ when: manual
+ only:
+ - branches
+
+# Trigger a docs build in gitlab-docs
+# Useful to preview the docs changes live
+review-docs-deploy:
+ <<: *review-docs
+ stage: build
+ environment:
+ name: review-docs/$CI_COMMIT_REF_NAME
+ on_stop: review-docs-cleanup
+ script:
+ - gem install gitlab --no-doc
+ - scripts/trigger-build-docs deploy
+
+# Cleanup remote environment of gitlab-docs
+review-docs-cleanup:
+ <<: *review-docs
+ stage: post-cleanup
+ environment:
+ name: review-docs/$CI_COMMIT_REF_NAME
+ action: stop
+ script:
+ - gem install gitlab --no-doc
+ - scripts/trigger-build-docs cleanup
+
+# Retrieve knapsack and rspec_flaky reports
+retrieve-tests-metadata:
+ <<: *tests-metadata-state
<<: *dedicated-runner
<<: *except-docs
stage: prepare
cache:
- key: knapsack
- paths:
- - knapsack/
+ key: tests_metadata
policy: pull
script:
- mkdir -p knapsack/${CI_PROJECT_NAME}/
- - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
- - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
+ - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
+ - 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}/
+ - 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}'
-update-knapsack:
- <<: *knapsack-state
+update-tests-metadata:
+ <<: *tests-metadata-state
<<: *dedicated-runner
<<: *only-canonical-masters
stage: post-test
cache:
- key: knapsack
+ key: tests_metadata
paths:
- knapsack/
+ - rspec_flaky/
policy: push
script:
- 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
- - '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
+ - scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/${CI_PROJECT_NAME}/all_node_*.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
+
+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
+ stage: post-test
+ allow_failure: yes
+ only:
+ - branches
+ except:
+ - master
+ - /(^docs[\/-].*|.*-docs$)/
+ artifacts:
+ expire_in: 30d
+ paths:
+ - 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/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT
setup-test-env:
<<: *use-pg
@@ -220,8 +282,7 @@ setup-test-env:
<<: *default-cache
script:
- node --version
- - yarn install --pure-lockfile --cache-folder .yarn-cache
- - bundle exec rake gettext:po_to_json
+ - yarn install --frozen-lockfile --cache-folder .yarn-cache
- bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
- scripts/gitaly-test-build # Do not use 'bundle exec' here
@@ -232,69 +293,69 @@ setup-test-env:
- public/assets
- tmp/tests
-rspec-pg 0 25: *rspec-knapsack-pg
-rspec-pg 1 25: *rspec-knapsack-pg
-rspec-pg 2 25: *rspec-knapsack-pg
-rspec-pg 3 25: *rspec-knapsack-pg
-rspec-pg 4 25: *rspec-knapsack-pg
-rspec-pg 5 25: *rspec-knapsack-pg
-rspec-pg 6 25: *rspec-knapsack-pg
-rspec-pg 7 25: *rspec-knapsack-pg
-rspec-pg 8 25: *rspec-knapsack-pg
-rspec-pg 9 25: *rspec-knapsack-pg
-rspec-pg 10 25: *rspec-knapsack-pg
-rspec-pg 11 25: *rspec-knapsack-pg
-rspec-pg 12 25: *rspec-knapsack-pg
-rspec-pg 13 25: *rspec-knapsack-pg
-rspec-pg 14 25: *rspec-knapsack-pg
-rspec-pg 15 25: *rspec-knapsack-pg
-rspec-pg 16 25: *rspec-knapsack-pg
-rspec-pg 17 25: *rspec-knapsack-pg
-rspec-pg 18 25: *rspec-knapsack-pg
-rspec-pg 19 25: *rspec-knapsack-pg
-rspec-pg 20 25: *rspec-knapsack-pg
-rspec-pg 21 25: *rspec-knapsack-pg
-rspec-pg 22 25: *rspec-knapsack-pg
-rspec-pg 23 25: *rspec-knapsack-pg
-rspec-pg 24 25: *rspec-knapsack-pg
-
-rspec-mysql 0 25: *rspec-knapsack-mysql
-rspec-mysql 1 25: *rspec-knapsack-mysql
-rspec-mysql 2 25: *rspec-knapsack-mysql
-rspec-mysql 3 25: *rspec-knapsack-mysql
-rspec-mysql 4 25: *rspec-knapsack-mysql
-rspec-mysql 5 25: *rspec-knapsack-mysql
-rspec-mysql 6 25: *rspec-knapsack-mysql
-rspec-mysql 7 25: *rspec-knapsack-mysql
-rspec-mysql 8 25: *rspec-knapsack-mysql
-rspec-mysql 9 25: *rspec-knapsack-mysql
-rspec-mysql 10 25: *rspec-knapsack-mysql
-rspec-mysql 11 25: *rspec-knapsack-mysql
-rspec-mysql 12 25: *rspec-knapsack-mysql
-rspec-mysql 13 25: *rspec-knapsack-mysql
-rspec-mysql 14 25: *rspec-knapsack-mysql
-rspec-mysql 15 25: *rspec-knapsack-mysql
-rspec-mysql 16 25: *rspec-knapsack-mysql
-rspec-mysql 17 25: *rspec-knapsack-mysql
-rspec-mysql 18 25: *rspec-knapsack-mysql
-rspec-mysql 19 25: *rspec-knapsack-mysql
-rspec-mysql 20 25: *rspec-knapsack-mysql
-rspec-mysql 21 25: *rspec-knapsack-mysql
-rspec-mysql 22 25: *rspec-knapsack-mysql
-rspec-mysql 23 25: *rspec-knapsack-mysql
-rspec-mysql 24 25: *rspec-knapsack-mysql
-
-spinach-pg 0 5: *spinach-knapsack-pg
-spinach-pg 1 5: *spinach-knapsack-pg
-spinach-pg 2 5: *spinach-knapsack-pg
-spinach-pg 3 5: *spinach-knapsack-pg
-spinach-pg 4 5: *spinach-knapsack-pg
-
-spinach-mysql 0 5: *spinach-knapsack-mysql
-spinach-mysql 1 5: *spinach-knapsack-mysql
-spinach-mysql 2 5: *spinach-knapsack-mysql
-spinach-mysql 3 5: *spinach-knapsack-mysql
-spinach-mysql 4 5: *spinach-knapsack-mysql
+rspec-pg 0 25: *rspec-metadata-pg
+rspec-pg 1 25: *rspec-metadata-pg
+rspec-pg 2 25: *rspec-metadata-pg
+rspec-pg 3 25: *rspec-metadata-pg
+rspec-pg 4 25: *rspec-metadata-pg
+rspec-pg 5 25: *rspec-metadata-pg
+rspec-pg 6 25: *rspec-metadata-pg
+rspec-pg 7 25: *rspec-metadata-pg
+rspec-pg 8 25: *rspec-metadata-pg
+rspec-pg 9 25: *rspec-metadata-pg
+rspec-pg 10 25: *rspec-metadata-pg
+rspec-pg 11 25: *rspec-metadata-pg
+rspec-pg 12 25: *rspec-metadata-pg
+rspec-pg 13 25: *rspec-metadata-pg
+rspec-pg 14 25: *rspec-metadata-pg
+rspec-pg 15 25: *rspec-metadata-pg
+rspec-pg 16 25: *rspec-metadata-pg
+rspec-pg 17 25: *rspec-metadata-pg
+rspec-pg 18 25: *rspec-metadata-pg
+rspec-pg 19 25: *rspec-metadata-pg
+rspec-pg 20 25: *rspec-metadata-pg
+rspec-pg 21 25: *rspec-metadata-pg
+rspec-pg 22 25: *rspec-metadata-pg
+rspec-pg 23 25: *rspec-metadata-pg
+rspec-pg 24 25: *rspec-metadata-pg
+
+rspec-mysql 0 25: *rspec-metadata-mysql
+rspec-mysql 1 25: *rspec-metadata-mysql
+rspec-mysql 2 25: *rspec-metadata-mysql
+rspec-mysql 3 25: *rspec-metadata-mysql
+rspec-mysql 4 25: *rspec-metadata-mysql
+rspec-mysql 5 25: *rspec-metadata-mysql
+rspec-mysql 6 25: *rspec-metadata-mysql
+rspec-mysql 7 25: *rspec-metadata-mysql
+rspec-mysql 8 25: *rspec-metadata-mysql
+rspec-mysql 9 25: *rspec-metadata-mysql
+rspec-mysql 10 25: *rspec-metadata-mysql
+rspec-mysql 11 25: *rspec-metadata-mysql
+rspec-mysql 12 25: *rspec-metadata-mysql
+rspec-mysql 13 25: *rspec-metadata-mysql
+rspec-mysql 14 25: *rspec-metadata-mysql
+rspec-mysql 15 25: *rspec-metadata-mysql
+rspec-mysql 16 25: *rspec-metadata-mysql
+rspec-mysql 17 25: *rspec-metadata-mysql
+rspec-mysql 18 25: *rspec-metadata-mysql
+rspec-mysql 19 25: *rspec-metadata-mysql
+rspec-mysql 20 25: *rspec-metadata-mysql
+rspec-mysql 21 25: *rspec-metadata-mysql
+rspec-mysql 22 25: *rspec-metadata-mysql
+rspec-mysql 23 25: *rspec-metadata-mysql
+rspec-mysql 24 25: *rspec-metadata-mysql
+
+spinach-pg 0 5: *spinach-metadata-pg
+spinach-pg 1 5: *spinach-metadata-pg
+spinach-pg 2 5: *spinach-metadata-pg
+spinach-pg 3 5: *spinach-metadata-pg
+spinach-pg 4 5: *spinach-metadata-pg
+
+spinach-mysql 0 5: *spinach-metadata-mysql
+spinach-mysql 1 5: *spinach-metadata-mysql
+spinach-mysql 2 5: *spinach-metadata-mysql
+spinach-mysql 3 5: *spinach-metadata-mysql
+spinach-mysql 4 5: *spinach-metadata-mysql
# Static analysis jobs
.ruby-static-analysis: &ruby-static-analysis
@@ -354,7 +415,7 @@ ee_compat_check:
except:
- master
- tags
- - /^[\d-]+-stable(-ee)?$/
+ - /^[\d-]+-stable(-ee)?/
allow_failure: yes
cache:
key: "ee_compat_check_repo"
@@ -386,7 +447,6 @@ db:migrate:reset-mysql:
.migration-paths: &migration-paths
<<: *dedicated-runner
- <<: *only-canonical-masters
<<: *pull-cache
stage: test
variables:
@@ -435,7 +495,6 @@ db:rollback-mysql:
variables:
SIZE: "1"
SETUP_DB: "false"
- RAILS_ENV: "development"
script:
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git
@@ -470,8 +529,7 @@ gitlab:assets:compile:
WEBPACK_REPORT: "true"
NO_COMPRESSION: "true"
script:
- - yarn install --pure-lockfile --production --cache-folder .yarn-cache
- - bundle exec rake gettext:po_to_json
+ - yarn install --frozen-lockfile --production --cache-folder .yarn-cache
- bundle exec rake gitlab:assets:compile
artifacts:
name: webpack-report
@@ -484,7 +542,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.7-chrome-59.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-60.0-node-7.1-postgresql-9.6"
stage: test
variables:
BABEL_ENV: "coverage"
@@ -514,8 +572,11 @@ codeclimate:
services:
- docker:dind
script:
+ - cp .rubocop.yml .rubocop.yml.bak
+ - grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
+ - mv .rubocop.yml.bak .rubocop.yml
artifacts:
paths: [codeclimate.json]
diff --git a/.gitlab/merge_request_templates/Database Changes.md b/.gitlab/merge_request_templates/Database Changes.md
new file mode 100644
index 00000000000..2a5c8267872
--- /dev/null
+++ b/.gitlab/merge_request_templates/Database Changes.md
@@ -0,0 +1,73 @@
+Remove this section and replace it with a description of your MR. Also follow the
+checklist below and check off any tasks that are done. If a certain task can not
+be done you should explain so in the MR body. You are free to remove any
+sections that do not apply to your MR.
+
+When gathering statistics (e.g. the output of `EXPLAIN ANALYZE`) you should make
+sure your database has enough data. Having around 10 000 rows in the tables
+being queries should provide a reasonable estimate of how a query will behave.
+Also make sure that PostgreSQL uses the following settings:
+
+* `random_page_cost`: `1`
+* `work_mem`: `16MB`
+* `maintenance_work_mem`: at least `64MB`
+* `shared_buffers`: at least `256MB`
+
+If you have access to GitLab.com's staging environment you should also run your
+measurements there, and include the results in this MR.
+
+## Database Checklist
+
+When adding migrations:
+
+- [ ] Updated `db/schema.rb`
+- [ ] Added a `down` method so the migration can be reverted
+- [ ] Added the output of the migration(s) to the MR body
+- [ ] Added the execution time of the migration(s) to the MR body
+- [ ] Added tests for the migration in `spec/migrations` if necessary (e.g. when
+ migrating data)
+- [ ] Made sure the migration won't interfere with a running GitLab cluster,
+ for example by disabling transactions for long running migrations
+
+When adding or modifying queries to improve performance:
+
+- [ ] Included the raw SQL queries of the relevant queries
+- [ ] Included the output of `EXPLAIN ANALYZE` and execution timings of the
+ relevant queries
+- [ ] Added tests for the relevant changes
+
+When adding indexes:
+
+- [ ] Described the need for these indexes in the MR body
+- [ ] Made sure existing indexes can not be reused instead
+
+When adding foreign keys to existing tables:
+
+- [ ] Included a migration to remove orphaned rows in the source table
+- [ ] Removed any instances of `dependent: ...` that may no longer be necessary
+
+When adding tables:
+
+- [ ] Ordered columns based on their type sizes in descending order
+- [ ] Added foreign keys if necessary
+- [ ] Added indexes if necessary
+
+When removing columns, tables, indexes or other structures:
+
+- [ ] Removed these in a post-deployment migration
+- [ ] Made sure the application no longer uses (or ignores) these structures
+
+## General Checklist
+
+- [ ] [Changelog entry](https://docs.gitlab.com/ce/development/changelog.html) added, if necessary
+- [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
+- [ ] API support added
+- [ ] Tests added for this feature/bug
+- Review
+ - [ ] Has been reviewed by UX
+ - [ ] Has been reviewed by Frontend
+ - [ ] Has been reviewed by Backend
+ - [ ] Has been reviewed by Database
+- [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
+- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
+- [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
diff --git a/.rubocop.yml b/.rubocop.yml
index a5ccec0437b..4640681379a 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,5 +1,6 @@
require:
- rubocop-rspec
+ - rubocop-gitlab-security
- ./rubocop/rubocop
inherit_from: .rubocop_todo.yml
@@ -206,6 +207,13 @@ Layout/SpaceAroundKeyword:
Layout/SpaceAroundOperators:
Enabled: true
+# Checks that block braces have or don't have a space before the opening
+# brace depending on configuration.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: space, no_space
+Layout/SpaceBeforeBlockBraces:
+ Enabled: true
+
# No spaces before commas.
Layout/SpaceBeforeComma:
Enabled: true
@@ -243,6 +251,10 @@ Layout/Tab:
Layout/TrailingBlankLines:
Enabled: true
+# Avoid trailing whitespace.
+Layout/TrailingWhitespace:
+ Enabled: true
+
# Style #######################################################################
# Check the naming of accessor methods for get_/set_.
@@ -594,13 +606,25 @@ Style/YodaCondition:
Style/Proc:
Enabled: true
+# Use `spam?` instead of `is_spam?`
+# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
+# NamePrefix: is_, has_, have_
+# NamePrefixBlacklist: is_, has_, have_
+# NameWhitelist: is_a?
+Style/PredicateName:
+ Enabled: true
+ NamePrefixBlacklist: is_
+ Exclude:
+ - 'spec/**/*'
+ - 'features/**/*'
+
# Metrics #####################################################################
# A calculated magnitude based on number of assignments,
# branches, and conditions.
Metrics/AbcSize:
Enabled: true
- Max: 56.96
+ Max: 55.25
# This cop checks if the length of a block exceeds some maximum value.
Metrics/BlockLength:
@@ -619,7 +643,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method.
Metrics/CyclomaticComplexity:
Enabled: true
- Max: 16
+ Max: 14
# Limit lines to 80 characters.
Metrics/LineLength:
@@ -641,7 +665,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity:
Enabled: true
- Max: 18
+ Max: 17
# Lint ########################################################################
@@ -1037,7 +1061,7 @@ RSpec/BeforeAfterAll:
RSpec/DescribeClass:
Enabled: false
-# Use `described_class` for tested class / module.
+# Checks that the second argument to `describe` specifies a method.
RSpec/DescribeMethod:
Enabled: false
@@ -1045,8 +1069,7 @@ RSpec/DescribeMethod:
RSpec/DescribeSymbol:
Enabled: true
-# Checks that the second argument to top level describe is the tested method
-# name.
+# Checks that tests use `described_class`.
RSpec/DescribedClass:
Enabled: true
@@ -1091,6 +1114,11 @@ RSpec/FilePath:
RSpec/Focus:
Enabled: true
+# Checks the arguments passed to `before`, `around`, and `after`.
+RSpec/HookArgument:
+ Enabled: true
+ EnforcedStyle: implicit
+
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: is_expected, should
RSpec/ImplicitExpect:
@@ -1156,3 +1184,43 @@ RSpec/SubjectStub:
# Prefer using verifying doubles over normal doubles.
RSpec/VerifiedDoubles:
Enabled: false
+
+# GitlabSecurity ##############################################################
+
+GitlabSecurity/DeepMunge:
+ Enabled: true
+ Exclude:
+ - 'lib/**/*.rake'
+ - 'spec/**/*'
+
+# To be enabled by https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13610
+GitlabSecurity/JsonSerialization:
+ Enabled: false
+
+GitlabSecurity/PublicSend:
+ Enabled: true
+ Exclude:
+ - 'config/**/*'
+ - 'db/**/*'
+ - 'features/**/*'
+ - 'lib/**/*.rake'
+ - 'qa/**/*'
+ - 'spec/**/*'
+
+GitlabSecurity/RedirectToParamsUpdate:
+ Enabled: true
+ Exclude:
+ - 'lib/**/*.rake'
+ - 'spec/**/*'
+
+GitlabSecurity/SqlInjection:
+ Enabled: true
+ Exclude:
+ - 'lib/**/*.rake'
+ - 'spec/**/*'
+
+GitlabSecurity/SystemCommandInjection:
+ Enabled: true
+ Exclude:
+ - 'lib/**/*.rake'
+ - 'spec/**/*'
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 9caef3bde08..cdf97d1d842 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -26,13 +26,6 @@ Layout/IndentArray:
Layout/IndentHash:
Enabled: false
-# Offense count: 174
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: space, no_space
-Layout/SpaceBeforeBlockBraces:
- Enabled: false
-
# Offense count: 8
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
@@ -64,11 +57,6 @@ Layout/SpaceInsideParens:
Layout/SpaceInsidePercentLiteralDelimiters:
Enabled: false
-# Offense count: 89
-# Cop supports --auto-correct.
-Layout/TrailingWhitespace:
- Enabled: false
-
# Offense count: 272
RSpec/EmptyLineAfterFinalLet:
Enabled: false
@@ -77,12 +65,6 @@ RSpec/EmptyLineAfterFinalLet:
RSpec/EmptyLineAfterSubject:
Enabled: false
-# Offense count: 78
-# Configuration parameters: EnforcedStyle, SupportedStyles.
-# SupportedStyles: implicit, each, example
-RSpec/HookArgument:
- Enabled: false
-
# Offense count: 9
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: it_behaves_like, it_should_behave_like
@@ -255,14 +237,6 @@ Style/PercentLiteralDelimiters:
Style/PerlBackrefs:
Enabled: false
-# Offense count: 105
-# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
-# NamePrefix: is_, has_, have_
-# NamePrefixBlacklist: is_, has_, have_
-# NameWhitelist: is_a?
-Style/PredicateName:
- Enabled: false
-
# Offense count: 58
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7493f2562e8..4cedfa60b3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,255 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 9.5.4 (2017-09-06)
+
+- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
+- [SECURITY] Prevent a persistent XSS in the commit author block.
+- Fix XSS issue in go-get handling.
+- Resolve CSRF token leakage via pathname manipulation on environments page.
+- Fixes race condition in project uploads.
+- Disallow arbitrary properties in `th` and `td` `style` attributes.
+- Disallow the `name` attribute on all user-provided markup.
+
+## 9.5.3 (2017-09-03)
+
+- [SECURITY] Filter additional secrets from Rails logs.
+- [FIXED] Make username update fail if the namespace update fails. !13642
+- [FIXED] Fix failure when issue is authored by a deleted user. !13807
+- [FIXED] Reverts changes made to signin_enabled. !13956
+- [FIXED] Fix Merge when pipeline succeeds button dropdown caret icon horizontal alignment.
+- [FIXED] Fixed diff changes bar buttons from showing/hiding whilst scrolling.
+- [FIXED] Fix events error importing GitLab projects.
+- [FIXED] Fix pipeline trigger via API fails with 500 Internal Server Error in 9.5.
+- [FIXED] Fixed fly-out nav flashing in & out.
+- [FIXED] Remove closing external issues by reference error.
+- [FIXED] Re-allow appearances.description_html to be NULL.
+- [CHANGED] Update and fix resolvable note icons for easier recognition.
+- [OTHER] Eager load head pipeline projects for MRs index.
+- [OTHER] Instrument MergeRequest#fetch_ref.
+- [OTHER] Instrument MergeRequest#ensure_ref_fetched.
+
+## 9.5.2 (2017-08-28)
+
+- [FIXED] Fix signing in using LDAP when attribute mapping uses simple strings instead of arrays.
+- [FIXED] Show un-highlighted text diffs when we do not have references to the correct blobs.
+- [FIXED] Fix display of push events for removed refs.
+- [FIXED] Testing of some integrations were broken due to missing ServiceHook record.
+- [FIXED] Fire system hooks when a user is created via LDAP.
+- [FIXED] Fix new project form not resetting the template value.
+
+## 9.5.1 (2017-08-23)
+
+- [FIXED] Fix merge request pipeline status when pipeline has errors. !13664
+- [FIXED] Commit rows would occasionally render with the wrong language.
+- [FIXED] Fix caching of future broadcast messages.
+- [FIXED] Only require Sidekiq throttling library when enabled, to reduce cache misses.
+- Raise Housekeeping timeout to 24 hours. !13719
+
+## 9.5.0 (2017-08-22)
+
+- [FIXED] Fix timeouts when creating projects in groups with many members. !13508
+- [FIXED] Improve API pagination headers when no record found. !13629 (Jordan Patterson)
+- [FIXED] Fix deleting GitLab Pages files when a project is removed. !13631
+- [FIXED] Fix commit list not loading the correct page when scrolling.
+- [OTHER] Cache the number of forks of a project. !13535
+- GPG signed commits integration. !9546 (Alexis Reigel)
+- Alert the user if a Wiki page changed while they were editing it in order to prevent overwriting changes. !9707 (Hiroyuki Sato)
+- Add custom linter for inline JavaScript to haml_lint. !9742 (winniehell)
+- Add /shrug and /tableflip commands. !10068 (Alex Ives)
+- Allow wiki pages to be renamed in the UI. !10069 (wendy0402)
+- Insert user name directly without encoding. !10085 (Nathan Neulinger <nneul@neulinger.org>)
+- Avoid plucking Todo ids in TodoService. !10845
+- Handle errors while a project is being deleted asynchronously. !11088
+- Decrease ABC threshold to 56.96. !11227 (Maxim Rydkin)
+- Remove Mattermost team when deleting a group. !11362
+- Block access to failing repository storage. !11449
+- Add coordinator url to admin area runner page. !11603
+- Allow testing any events for project hooks and system hooks. !11728 (Alexander Randa (@randaalex))
+- Disallow running the pipeline if ref is protected and user cannot merge the branch or create the tag. !11910
+- Remove project_key from the Jira configuration. !12050
+- Add CSRF token verification to API. !12154 (Vitaliy @blackst0ne Klachkov)
+- Fixes needed when GitLab sign-in is not enabled. !12491 (Robin Bobbitt)
+- Lazy load images for better Frontend performance. !12503
+- Replaces dashboard/event_filters.feature spinach with rspec. !12651 (Alexander Randa (@randaalex))
+- Toggle import description with import_sources_enabled. !12691 (Brianna Kicia)
+- Bump scss-lint to 0.54.0. !12733 (Takuya Noguchi)
+- Enable SpaceAfterComma in scss-lint. !12734 (Takuya Noguchi)
+- Remove CSS for nprogress removed. !12737 (Takuya Noguchi)
+- Enable UnnecessaryParentReference in scss-lint. !12738 (Takuya Noguchi)
+- Extract "@request.env[devise.mapping] = Devise.mappings[:user]" to a test helper. !12742 (Jacopo Beschi @jacopo-beschi)
+- Enable ImportPath in scss-lint. !12749 (Takuya Noguchi)
+- Enable PropertySpelling in scss-lint. !12752 (Takuya Noguchi)
+- Add API for protected branches to allow for wildcard matching and no access restrictions. !12756 (Eric Yu)
+- refactor initializations in dropzone_input.js. !12768 (Brandon Everett)
+- Improve CSS for global nav dropdown UI. !12772 (Takuya Noguchi)
+- Remove public/ci/favicon.ico. !12803 (Takuya Noguchi)
+- Enable DeclarationOrder in scss-lint. !12805 (Takuya Noguchi)
+- Increase width of dropdown menus automatically. !12809 (Thomas Wucher)
+- Enable BangFormat in scss-lint [ci skip]. !12815 (Takuya Noguchi)
+- Added /duplicate quick action to close a duplicate issue. !12845 (Ryan Scott)
+- Make all application-settings accessible through the API. !12851
+- Remove Inactive Personal Access Tokens list from Access Tokens page. !12866
+- Replaces dashboard/dashboard.feature spinach with rspec. !12876 (Alexander Randa (@randaalex))
+- Reduce memory usage of the GitHub importer. !12886
+- Bump fog-core to 1.44.3 and fog providers' plugins to latest. !12897 (Takuya Noguchi)
+- Use only CSS to truncate commit message in blame. !12900 (Takuya Noguchi)
+- Protect manual actions against protected tag too. !12908
+- Allow to configure automatic retry of a failed CI/CD job. !12909
+- Remove help message about prioritized labels for non-members. !12912 (Takuya Noguchi)
+- Add link to doc/api/ci/lint.md. !12914 (Takuya Noguchi)
+- Add RequestCache which makes caching with RequestStore easier. !12920
+- Free up some top level words, reject top level groups named like files in the public folder. !12932
+- Extend API for Group Secret Variable. !12936
+- Hide description about protected branches to non-member. !12945 (Takuya Noguchi)
+- Support custom directory in gitlab:backup:create task. !12984 (Markus Koller)
+- Raise guessed encoding confidence threshold to 50. !12990
+- Add author_id & assignee_id param to /issues API. !13004
+- Fix today day highlight in calendar. !13048
+- Prevent LDAP login callback from being called with a GET request. !13059
+- Add top-level merge_requests API endpoint. !13060
+- Handle maximum pages artifacts size correctly. !13072
+- Enable gitaly_post_upload_pack by default. !13078
+- Add Prometheus metrics exporter to Sidekiq. !13082
+- Fix improperly skipped backups of wikis. !13096
+- Projects can be created from templates. !13108
+- Fix the /projects/:id/repository/branches endpoint to handle dots in the branch name when the project full path contains a `/`. !13115
+- Fix project logos that are not centered vertically on list pages. !13124 (Florian Lemaitre)
+- Derive project path from import URL. !13131
+- Fix deletion of deploy keys linked to other projects. !13162
+- repository archive download url now ends with selected file extension. !13178 (haseebeqx)
+- Show auto-generated avatars for Groups without avatars. !13188
+- Allow any logged in users to read_users_list even if it's restricted. !13201
+- Unlock stuck merge request and set the proper state. !13207
+- Fix timezone inconsistencies in user contribution graph. !13208
+- Fix Issue board when using Ruby 2.4. !13220
+- Don't rename namespace called system when upgrading from 9.1.x to 9.5. !13228
+- Fix encoding error for WebHook logging. !13230 (Alexander Randa (@randaalex))
+- Uniquify reserved word usernames on OAuth user creation. !13244 (Robin Bobbitt)
+- Expose target_iid in Events API. !13247 (sue445)
+- Add star for action scope, in order to delete image from registry. !13248 (jean)
+- Make Delete Merged Branches handle wildcard protected branches correctly. !13251
+- Fix an order of operations for CI connection error message in merge request widget. !13252
+- Don't send rejection mails for all auto-generated mails. !13254
+- Expose noteable_iid in Note. !13265 (sue445)
+- Fix pipeline_schedules pages when active schedule has an abnormal state. !13286
+- Move some code from services to workers in order to improve performance. !13326
+- Fix destroy of case-insensitive conflicting redirects. !13357
+- Fix the /projects/:id/repository/tags endpoint to handle dots in the tag name when the project full path contains a `/`. !13368
+- Fix the /projects/:id/repository/commits endpoint to handle dots in the ref name when the project full path contains a `/`. !13370
+- Project pending delete no longer return 500 error in admins projects view. !13389
+- Use full path of user's avatar in webhooks. !13401 (Vitaliy @blackst0ne Klachkov)
+- Make GPGME temporary directory handling thread safe. !13481 (Alexis Reigel)
+- Add support for kube_namespace in Metrics queries. !16169
+- Fix bar chart does not display label at 0 hour. !35136 (Jason Dai)
+- Use project_ref_path to create the link to a branch to fix links that 404.
+- Declare related resources into V4 API entities.
+- Add Slack and JIRA services counts to Usage Data.
+- Prevent web hook and project service background jobs from going to the dead jobs queue.
+- Display specific error message when JIRA test fails.
+- clean up merge request widget UI.
+- Associate Issues tab only with internal issues tracker.
+- Remove events column from notification settings table.
+- Clarifies and rearranges the input variables on the kubernetes integration page and adjusts the docs slightly to meet the same order.
+- Respect blockquote line breaks in markdown.
+- Update confidential issue UI - add confidential visibility and settings to sidebar.
+- Add icons to contextual sidebars.
+- Make contextual sidebar collapsible.
+- Update Pipeline's badge count in Merge Request and Commits view to match real-time content.
+- Added link to the MR widget that directs to the monitoring dashboard.
+- Use jQuery to control scroll behavior in job log for cross browser consistency.
+- move edit comment button outside of dropdown.
+- Updates vue resource and code according to breaking changes.
+- Add GitHub imported projects count to usage data.
+- Rename about to overview for group and project page.
+- Prevent disabled pagination button to be clicked.
+- Remove coffee-rails gem. (Takuya Noguchi)
+- Remove net-ssh gem. (Takuya Noguchi)
+- Bump rubocop to 0.49.1 and rubocop-rspec to 1.15.1. (Takuya Noguchi)
+- improve file upload/replace experience.
+- allow closing Cycle Analytics intro box in firefox.
+- Fix label creation from new list for subgroup projects.
+- fix transient js error in rspec tests.
+- fix jump to next discussion button.
+- Fix translations for Star/Unstar in JS file.
+- Improve mobile sidebar.
+- Rename Pipelines tab to CI / CD in new navigation.
+- Fix display of new diff comments after changing b between diff views.
+- Store & use ConvDev percentages returned by the Version app.
+- Fixes new issue button for failed job returning 404.
+- Align OR separator to center in new project page.
+- Add filtered search to group issue dashboard.
+- Cache Appearance instances in Redis.
+- Fixed breadcrumbs title aggressively collapsing.
+- Better caching and indexing of broadcast messages.
+- Moved diff changed files into a dropdown.
+- Improve performance of large (initial) push into default branch.
+- Improve performance of checking for projects on the projects dashboard.
+- Eager load project creators for project dashboards.
+- Modify if condition to be more readable.
+- Fix links to group milestones from issue and merge request sidebar.
+- Remove hidden symlinks from project import files.
+- Fixed sign-in restrictions buttons not toggling active state.
+- Fix replying to commit comments on merge requests created from forks.
+- Support Markdown references, autocomplete, and quick actions for group milestones.
+- Cache recent projects for group-level new resource creation.
+- Fix API responses when dealing with txt files.
+- Fix project milestones import when projects belongs to a group.
+- Fix Mattermost integration.
+- Memoize the number of personal projects a user has to reduce COUNT queries.
+- Merge issuable "reopened" state into "opened".
+- Migrate events into a new format to reduce the storage necessary and improve performance.
+- MR branch link now links to tree instead of commits.
+- Use Prev/Next pagination for exploring projects.
+- Pass before_script and script as-is preserving arrays.
+- Change project FK migration to skip existing FKs.
+- Remove redundant query when retrieving the most recent push of a user.
+- Re-organise "issues" indexes for faster ordering.
+- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
+- Fix search box losing focus when typing.
+- Add structured logging for Rails processes.
+- Skip oAuth authorization for trusted applications.
+- Use a specialized class for querying events to improve performance.
+- Update build badges to be pipeline badges and display passing instead of success.
+
+## 9.4.6 (2017-09-06)
+
+- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
+- [SECURITY] Prevent a persistent XSS in the commit author block.
+- Fix XSS issue in go-get handling.
+- Remove hidden symlinks from project import files.
+- Fixes race condition in project uploads.
+- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
+- Disallow arbitrary properties in `th` and `td` `style` attributes.
+- Resolve CSRF token leakage via pathname manipulation on environments page.
+- Disallow the `name` attribute on all user-provided markup.
+
+## 9.4.5 (2017-08-14)
+
+- Fix deletion of deploy keys linked to other projects. !13162
+- Allow any logged in users to read_users_list even if it's restricted. !13201
+- Make Delete Merged Branches handle wildcard protected branches correctly. !13251
+- Fix an order of operations for CI connection error message in merge request widget. !13252
+- Fix pipeline_schedules pages when active schedule has an abnormal state. !13286
+- Add missing validation error for username change with container registry tags. !13356
+- Fix destroy of case-insensitive conflicting redirects. !13357
+- Project pending delete no longer return 500 error in admins projects view. !13389
+- Fix search box losing focus when typing.
+- Use jQuery to control scroll behavior in job log for cross browser consistency.
+- Use project_ref_path to create the link to a branch to fix links that 404.
+- improve file upload/replace experience.
+- fix jump to next discussion button.
+- Fixes new issue button for failed job returning 404.
+- Fix links to group milestones from issue and merge request sidebar.
+- Fixed sign-in restrictions buttons not toggling active state.
+- Fix Mattermost integration.
+- Change project FK migration to skip existing FKs.
+
+## 9.4.4 (2017-08-09)
+
+- Remove hidden symlinks from project import files.
+- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
+
## 9.4.3 (2017-07-31)
- Fix Prometheus client PID reuse bug. !13130
@@ -226,6 +475,29 @@ entry.
- Log rescued exceptions to Sentry.
- Remove remaining N+1 queries in merge requests API with emojis and labels.
+## 9.3.11 (2017-09-06)
+
+- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
+- [SECURITY] Prevent a persistent XSS in the commit author block.
+- Improve support for external issue references. !12485
+- Use uploads/system directory for personal snippets.
+- Remove uploads/appearance symlink. A leftover from a previous migration.
+- Fix XSS issue in go-get handling.
+- Remove hidden symlinks from project import files.
+- Fix an infinite loop when handling user-supplied regular expressions.
+- Fixes race condition in project uploads.
+- Fixes race condition in project uploads.
+- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
+- Disallow arbitrary properties in `th` and `td` `style` attributes.
+- Resolve CSRF token leakage via pathname manipulation on environments page.
+- Disallow the `name` attribute on all user-provided markup.
+- Renders 404 if given project is not readable by the user on Todos dashboard.
+
+## 9.3.10 (2017-08-09)
+
+- Remove hidden symlinks from project import files.
+- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
+
## 9.3.9 (2017-07-20)
- Fix an infinite loop when handling user-supplied regular expressions.
@@ -498,6 +770,11 @@ entry.
- Remove foreigh key on ci_trigger_schedules only if it exists.
- Allow translation of Pipeline Schedules.
+## 9.2.10 (2017-08-09)
+
+- Remove hidden symlinks from project import files.
+- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
+
## 9.2.9 (2017-07-20)
- Fix an infinite loop when handling user-supplied regular expressions.
@@ -753,6 +1030,11 @@ entry.
- Fix preemptive scroll bar on user activity calendar.
- Pipeline chat notifications convert seconds to minutes and hours.
+## 9.1.10 (2017-08-09)
+
+- Remove hidden symlinks from project import files.
+- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
+
## 9.1.9 (2017-07-20)
- Fix an infinite loop when handling user-supplied regular expressions.
@@ -1076,6 +1358,11 @@ entry.
- Only send chat notifications for the default branch.
- Don't fill in the default kubernetes namespace.
+## 9.0.13 (2017-08-09)
+
+- Remove hidden symlinks from project import files.
+- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
+
## 9.0.12 (2017-07-20)
- Fix an infinite loop when handling user-supplied regular expressions.
@@ -1456,6 +1743,11 @@ entry.
- Change development tanuki favicon colors to match logo color order.
- API issues - support filtering by iids.
+## 8.17.8 (2017-08-09)
+
+- Remove hidden symlinks from project import files.
+- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
+
## 8.17.7 (2017-07-19)
- Renders 404 if given project is not readable by the user on Todos dashboard.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 12fb34b24be..6fb2c6bd1dc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -5,7 +5,7 @@ By submitting code as an individual you agree to the
By submitting code as an entity you agree to the
[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
-_This notice should stay as the first item in the CONTRIBUTING.MD file._
+_This notice should stay as the first item in the CONTRIBUTING.md file._
---
@@ -21,7 +21,7 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._
- [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- - [Team labels (~CI, ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc)
+ - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc)
- [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements)
@@ -115,7 +115,7 @@ Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
-- Team: ~CI, ~Discussion, ~Edge, ~Platform, etc.
+- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
- Priority: ~Deliverable, ~Stretch
All labels, their meaning and priority are defined on the
@@ -157,14 +157,14 @@ Examples of subject labels are ~wiki, ~"container registry", ~ldap, ~api,
Subject labels are always all-lowercase.
-### Team labels (~CI, ~Discussion, ~Edge, ~Platform, etc.)
+### Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)
Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate
people.
-The current team labels are ~Build, ~CI, ~Discussion, ~Documentation, ~Edge,
-~Gitaly, ~Platform, ~Prometheus, ~Release, and ~"UX".
+The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge,
+~Geo, ~Gitaly, ~Platform, ~Prometheus, ~Release, and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
@@ -217,11 +217,11 @@ After adding the ~"Accepting Merge Requests" label, we try to estimate the
[weight](#issue-weight) of the issue. We use issue weight to let contributors
know how difficult the issue is. Additionally:
-- We advertise [~"Accepting Merge Requests" issues with weight < 5][up-for-grabs]
+- We advertise ["Accepting Merge Requests" issues with weight < 5][up-for-grabs]
as suitable for people that have never contributed to GitLab before on the
[Up For Grabs campaign](http://up-for-grabs.net)
- 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]
+ look for ["Accepting Merge Requests" issues with a weight of 1][firt-timers]
[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
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index ae6dd4e2032..ca75280b09b 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.29.0
+0.38.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 11d9efa3d5a..b3d91f9cfc0 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-5.8.0
+5.9.0
diff --git a/Gemfile b/Gemfile
index 08564ac0cae..a022319ae2c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -16,7 +16,7 @@ gem 'mysql2', '~> 0.4.5', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.26.0'
-gem 'grape-route-helpers', '~> 2.0.0'
+gem 'grape-route-helpers', '~> 2.1.0'
gem 'faraday', '~> 0.12'
@@ -27,11 +27,11 @@ gem 'doorkeeper-openid_connect', '~> 1.1.0'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
-gem 'omniauth-cas3', '~> 1.1.2'
+gem 'omniauth-cas3', '~> 1.1.4'
gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.2'
-gem 'omniauth-google-oauth2', '~> 0.4.1'
+gem 'omniauth-google-oauth2', '~> 0.5.2'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.7.0'
@@ -64,7 +64,7 @@ gem 'gpgme'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
-gem 'gitlab_omniauth-ldap', '~> 2.0.3', require: 'omniauth-ldap'
+gem 'gitlab_omniauth-ldap', '~> 2.0.4', require: 'omniauth-ldap'
gem 'net-ldap'
# Git Wiki
@@ -76,7 +76,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.4', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API
-gem 'grape', '~> 0.19.2'
+gem 'grape', '~> 1.0'
gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
@@ -84,7 +84,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
gem 'hashie-forbidden_attributes'
# Pagination
-gem 'kaminari', '~> 0.17.0'
+gem 'kaminari', '~> 1.0'
# HAML
gem 'hamlit', '~> 2.6.1'
@@ -126,12 +126,9 @@ gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7'
gem 'rouge', '~> 2.0'
-gem 'truncato', '~> 0.7.8'
+gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0'
-
-# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
-# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
-gem 'nokogiri', '~> 1.6.7', '>= 1.6.7.2'
+gem 'nokogiri', '~> 1.8.0'
# Diffs
gem 'diffy', '~> 3.1.0'
@@ -144,8 +141,6 @@ end
# State machine
gem 'state_machines-activerecord', '~> 0.4.0'
-# Run events after state machine commits
-gem 'after_commit_queue', '~> 1.3.0'
# Issue tags
gem 'acts-as-taggable-on', '~> 4.0'
@@ -154,7 +149,7 @@ gem 'acts-as-taggable-on', '~> 4.0'
gem 'sidekiq', '~> 5.0'
gem 'sidekiq-cron', '~> 0.6.0'
gem 'redis-namespace', '~> 1.5.2'
-gem 'sidekiq-limit_fetch', '~> 3.4'
+gem 'sidekiq-limit_fetch', '~> 3.4', require: false
# Cron Parser
gem 'rufus-scheduler', '~> 3.4'
@@ -186,7 +181,7 @@ gem 'connection_pool', '~> 2.0'
gem 'hipchat', '~> 1.5.0'
# JIRA integration
-gem 'jira-ruby', '~> 1.1.2'
+gem 'jira-ruby', '~> 1.4'
# Flowdock integration
gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
@@ -209,9 +204,6 @@ gem 'kubeclient', '~> 2.2.0'
# d3
gem 'd3_rails', '~> 3.5.0'
-# underscore-rails
-gem 'underscore-rails', '~> 1.8.0'
-
# Sanitize user input
gem 'sanitize', '~> 2.0'
gem 'babosa', '~> 1.0.2'
@@ -232,7 +224,7 @@ gem 'ace-rails-ap', '~> 4.1.0'
gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
-gem 'charlock_holmes', '~> 0.7.3'
+gem 'charlock_holmes', '~> 0.7.5'
# Faster JSON
gem 'oj', '~> 2.17.4'
@@ -250,7 +242,7 @@ gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.7'
-gem 'gemojione', '~> 3.0'
+gem 'gemojione', '~> 3.3'
gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0'
@@ -289,7 +281,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
- gem 'prometheus-client-mmap', '~>0.7.0.beta11'
+ gem 'prometheus-client-mmap', '~>0.7.0.beta14'
gem 'raindrops', '~> 0.18'
end
@@ -324,6 +316,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3'
+ gem 'rspec-parameterized'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
@@ -331,7 +324,7 @@ group :development, :test do
# Generate Fake data
gem 'ffaker', '~> 2.4'
- gem 'capybara', '~> 2.6.2'
+ gem 'capybara', '~> 2.15.0'
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0'
@@ -341,6 +334,7 @@ group :development, :test do
gem 'rubocop', '~> 0.49.1', require: false
gem 'rubocop-rspec', '~> 1.15.1', require: false
+ gem 'rubocop-gitlab-security', '~> 0.1.0', require: false
gem 'scss_lint', '~> 0.54.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false
@@ -354,7 +348,9 @@ group :development, :test do
gem 'activerecord_sane_schema_dumper', '0.2'
- gem 'stackprof', '~> 0.2.10'
+ gem 'stackprof', '~> 0.2.10', require: false
+
+ gem 'simple_po_parser', '~> 1.1.2', require: false
end
group :test do
@@ -396,12 +392,12 @@ gem 'net-ssh', '~> 4.1.0'
# Required for ED25519 SSH host key support
group :ed25519 do
gem 'rbnacl-libsodium'
- gem 'rbnacl', '~> 3.2'
+ gem 'rbnacl', '~> 4.0'
gem 'bcrypt_pbkdf', '~> 1.0'
end
# Gitaly GRPC client
-gem 'gitaly', '~> 0.26.0'
+gem 'gitaly-proto', '~> 0.33.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
@@ -411,3 +407,4 @@ gem 'flipper-active_record', '~> 0.10.2'
# Structured logging
gem 'lograge', '~> 0.5'
+gem 'grape_logging', '~> 1.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 948ba02a72c..d7e1c7581d5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,6 +2,7 @@ GEM
remote: https://rubygems.org/
specs:
RedCloth (4.3.2)
+ abstract_type (0.0.7)
ace-rails-ap (4.1.2)
actionmailer (4.2.8)
actionpack (= 4.2.8)
@@ -41,9 +42,10 @@ GEM
tzinfo (~> 1.1)
acts-as-taggable-on (4.0.0)
activerecord (>= 4.0)
+ adamantium (0.2.0)
+ ice_nine (~> 0.11.0)
+ memoizable (~> 0.4.0)
addressable (2.3.8)
- after_commit_queue (1.3.0)
- activerecord (>= 3.0)
akismet (2.0.0)
allocations (1.0.5)
arel (6.0.4)
@@ -98,9 +100,9 @@ GEM
bundler (~> 1.2)
thor (~> 0.18)
byebug (9.0.6)
- capybara (2.6.2)
+ capybara (2.15.1)
addressable
- mime-types (>= 1.16)
+ mini_mime (>= 0.1.3)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
@@ -113,7 +115,7 @@ GEM
activesupport (>= 4.0.0)
mime-types (>= 1.16)
cause (0.1)
- charlock_holmes (0.7.3)
+ charlock_holmes (0.7.5)
chronic (0.10.2)
chronic_duration (0.10.6)
numerizer (~> 0.1.1)
@@ -124,6 +126,9 @@ GEM
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
colorize (0.7.7)
+ concord (0.1.5)
+ adamantium (~> 0.2.0)
+ equalizer (~> 0.0.9)
concurrent-ruby (1.0.5)
concurrent-ruby-ext (1.0.5)
concurrent-ruby (= 1.0.5)
@@ -198,7 +203,7 @@ GEM
multi_json
fast_gettext (1.4.0)
ffaker (2.4.0)
- ffi (1.9.10)
+ ffi (1.9.18)
flay (2.8.1)
erubis (~> 2.7.0)
path_expander (~> 1.0)
@@ -256,7 +261,7 @@ GEM
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
- gemojione (3.0.1)
+ gemojione (3.3.0)
json
get_process_mem (0.2.0)
gettext (3.2.2)
@@ -270,7 +275,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly (0.26.0)
+ gitaly-proto (0.33.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -278,7 +283,7 @@ GEM
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
rugged (>= 0.23.0b)
- github-markup (1.4.0)
+ github-markup (1.6.1)
gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
@@ -289,7 +294,7 @@ GEM
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
gitlab-markup (1.5.1)
- gitlab_omniauth-ldap (2.0.3)
+ gitlab_omniauth-ldap (2.0.4)
net-ldap (~> 0.16)
omniauth (~> 1.3)
pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
@@ -298,13 +303,14 @@ GEM
activesupport (>= 4.1.0)
gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
- gollum-lib (4.2.1)
- github-markup (~> 1.4.0)
+ gollum-lib (4.2.7)
+ gemojione (~> 3.2)
+ github-markup (~> 1.6)
gollum-grit_adapter (~> 1.0)
- nokogiri (~> 1.6.4)
- rouge (~> 2.0)
- sanitize (~> 2.1.0)
- stringex (~> 2.5.1)
+ nokogiri (>= 1.6.1, < 2.0)
+ rouge (~> 2.1)
+ sanitize (~> 2.1)
+ stringex (~> 2.6)
gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15)
rugged (~> 0.25)
@@ -324,7 +330,7 @@ GEM
multi_json (~> 1.10)
retriable (~> 1.4)
signet (~> 0.6)
- google-protobuf (3.3.0)
+ google-protobuf (3.4.0.2)
googleauth (0.5.1)
faraday (~> 0.9)
jwt (~> 1.4)
@@ -335,12 +341,9 @@ GEM
signet (~> 0.7)
gpgme (2.0.13)
mini_portile2 (~> 2.1)
- grape (0.19.2)
+ grape (1.0.0)
activesupport
builder
- hashie (>= 2.1.0)
- multi_json (>= 1.3.2)
- multi_xml (>= 0.5.2)
mustermann-grape (~> 1.0.0)
rack (>= 1.3.0)
rack-accept
@@ -348,11 +351,13 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
- grape-route-helpers (2.0.0)
+ grape-route-helpers (2.1.0)
activesupport
- grape (~> 0.16, >= 0.16.0)
+ grape (>= 0.16.0)
rake
- grpc (1.4.0)
+ grape_logging (1.6.0)
+ grape
+ grpc (1.4.5)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
@@ -368,7 +373,7 @@ GEM
thor
tilt
hashdiff (0.3.4)
- hashie (3.5.5)
+ hashie (3.5.6)
hashie-forbidden_attributes (0.1.1)
hashie (>= 3.0)
health_check (2.6.0)
@@ -401,8 +406,9 @@ GEM
cause
json
ipaddress (0.8.3)
- jira-ruby (1.1.2)
+ jira-ruby (1.4.1)
activesupport
+ multipart-post
oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2)
jquery-rails (4.1.1)
@@ -419,9 +425,18 @@ GEM
json-schema (2.6.2)
addressable (~> 2.3.8)
jwt (1.5.6)
- kaminari (0.17.0)
- actionpack (>= 3.0.0)
- activesupport (>= 3.0.0)
+ kaminari (1.0.1)
+ activesupport (>= 4.1.0)
+ kaminari-actionview (= 1.0.1)
+ kaminari-activerecord (= 1.0.1)
+ kaminari-core (= 1.0.1)
+ kaminari-actionview (1.0.1)
+ actionview
+ kaminari-core (= 1.0.1)
+ kaminari-activerecord (1.0.1)
+ activerecord
+ kaminari-core (= 1.0.1)
+ kaminari-core (1.0.1)
kgio (2.10.0)
knapsack (1.11.0)
rake
@@ -457,14 +472,17 @@ GEM
railties (>= 4, < 5.2)
loofah (2.0.3)
nokogiri (>= 1.5.9)
- mail (2.6.5)
+ mail (2.6.6)
mime-types (>= 1.16, < 4)
mail_room (0.9.1)
memoist (0.15.0)
+ memoizable (0.4.2)
+ thread_safe (~> 0.3, >= 0.3.1)
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
- mini_portile2 (2.1.0)
+ mini_mime (0.1.4)
+ mini_portile2 (2.2.0)
minitest (5.7.0)
mmap2 (2.2.7)
mousetrap-rails (1.4.6)
@@ -478,8 +496,8 @@ GEM
net-ldap (0.16.0)
net-ssh (4.1.0)
netrc (0.11.0)
- nokogiri (1.6.8.1)
- mini_portile2 (~> 2.1.0)
+ nokogiri (1.8.0)
+ mini_portile2 (~> 2.2.0)
numerizer (0.1.1)
oauth (0.5.1)
oauth2 (1.4.0)
@@ -502,9 +520,9 @@ GEM
jwt (~> 1.0)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
- omniauth-cas3 (1.1.3)
+ omniauth-cas3 (1.1.4)
addressable (~> 2.3)
- nokogiri (~> 1.6.6)
+ nokogiri (~> 1.7, >= 1.7.1)
omniauth (~> 1.2)
omniauth-facebook (4.0.0)
omniauth-oauth2 (~> 1.2)
@@ -514,8 +532,8 @@ GEM
omniauth-gitlab (1.0.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
- omniauth-google-oauth2 (0.4.1)
- jwt (~> 1.5.2)
+ omniauth-google-oauth2 (0.5.2)
+ jwt (~> 1.5)
multi_json (~> 1.3)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.3.1)
@@ -548,7 +566,7 @@ GEM
rubypants (~> 0.2)
orm_adapter (0.5.0)
os (0.9.6)
- parallel (1.11.2)
+ parallel (1.12.0)
paranoia (2.3.1)
activerecord (>= 4.0, < 5.2)
parser (2.4.0.0)
@@ -592,7 +610,7 @@ GEM
cliver (~> 0.3.1)
multi_json (~> 1.0)
websocket-driver (>= 0.2.0)
- posix-spawn (0.3.11)
+ posix-spawn (0.3.13)
powerpack (0.1.1)
premailer (1.10.4)
addressable
@@ -601,7 +619,12 @@ GEM
premailer-rails (1.9.7)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
- prometheus-client-mmap (0.7.0.beta11)
+ proc_to_ast (0.1.0)
+ coderay
+ parser
+ unparser
+ procto (0.0.3)
+ prometheus-client-mmap (0.7.0.beta14)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
coderay (~> 1.1.0)
@@ -664,7 +687,7 @@ GEM
rake (12.0.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
- rbnacl (3.4.0)
+ rbnacl (4.0.2)
ffi
rbnacl-libsodium (1.0.11)
rbnacl (>= 3.0.1)
@@ -704,11 +727,15 @@ GEM
retriable (1.4.1)
rinku (2.0.0)
rotp (2.1.2)
- rouge (2.1.0)
+ rouge (2.2.1)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
rqrcode (>= 0.4.2)
+ rspec (3.6.0)
+ rspec-core (~> 3.6.0)
+ rspec-expectations (~> 3.6.0)
+ rspec-mocks (~> 3.6.0)
rspec-core (3.6.0)
rspec-support (~> 3.6.0)
rspec-expectations (3.6.0)
@@ -717,6 +744,12 @@ GEM
rspec-mocks (3.6.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.6.0)
+ rspec-parameterized (0.4.0)
+ binding_of_caller
+ parser
+ proc_to_ast
+ rspec (>= 2.13, < 4)
+ unparser
rspec-rails (3.6.0)
actionpack (>= 3.0)
activesupport (>= 3.0)
@@ -741,6 +774,8 @@ GEM
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
+ rubocop-gitlab-security (0.1.0)
+ rubocop (>= 0.47.1)
rubocop-rspec (1.15.1)
rubocop (>= 0.42.0)
ruby-fogbugz (0.2.1)
@@ -802,6 +837,7 @@ GEM
faraday (~> 0.9)
jwt (~> 1.5)
multi_json (~> 1.10)
+ simple_po_parser (1.1.2)
simplecov (0.14.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
@@ -841,7 +877,7 @@ GEM
state_machines-activerecord (0.4.0)
activerecord (>= 4.1, < 5.1)
state_machines-activemodel (>= 0.3.0)
- stringex (2.5.2)
+ stringex (2.7.1)
sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
@@ -860,16 +896,15 @@ GEM
timfel-krb5-auth (0.8.3)
toml-rb (0.3.15)
citrus (~> 3.0, > 3.0)
- truncato (0.7.8)
+ truncato (0.7.10)
htmlentities (~> 4.3.1)
- nokogiri (~> 1.6.1)
+ nokogiri (~> 1.8.0, >= 1.7.0)
tzinfo (1.2.3)
thread_safe (~> 0.1)
u2f (0.2.1)
uglifier (2.7.2)
execjs (>= 0.3.0)
json (>= 1.8.0)
- underscore-rails (1.8.3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
@@ -881,6 +916,14 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
+ unparser (0.2.6)
+ abstract_type (~> 0.0.7)
+ adamantium (~> 0.2.0)
+ concord (~> 0.1.5)
+ diff-lcs (~> 1.3)
+ equalizer (~> 0.0.9)
+ parser (>= 2.3.1.2, < 2.5)
+ procto (~> 0.0.2)
url_safe_base64 (0.2.2)
validates_hostname (1.0.6)
activerecord (>= 3.0)
@@ -908,7 +951,7 @@ GEM
expression_parser
rinku
xml-simple (1.1.5)
- xpath (2.0.0)
+ xpath (2.1.0)
nokogiri (~> 1.3)
PLATFORMS
@@ -920,7 +963,6 @@ DEPENDENCIES
activerecord_sane_schema_dumper (= 0.2)
acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8)
- after_commit_queue (~> 1.3.0)
akismet (~> 2.0)
allocations (~> 1.0)
asana (~> 0.6.0)
@@ -940,10 +982,10 @@ DEPENDENCIES
browser (~> 2.2)
bullet (~> 5.5.0)
bundler-audit (~> 0.5.0)
- capybara (~> 2.6.2)
+ capybara (~> 2.15.0)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 1.1)
- charlock_holmes (~> 0.7.3)
+ charlock_holmes (~> 0.7.5)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
concurrent-ruby (~> 1.0.5)
@@ -978,23 +1020,24 @@ DEPENDENCIES
foreman (~> 0.78.0)
fuubar (~> 2.2.0)
gemnasium-gitlab-service (~> 0.2)
- gemojione (~> 3.0)
+ gemojione (~> 3.3)
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly (~> 0.26.0)
+ gitaly-proto (~> 0.33.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
- gitlab_omniauth-ldap (~> 2.0.3)
+ 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)
gpgme
- grape (~> 0.19.2)
+ grape (~> 1.0)
grape-entity (~> 0.6.0)
- grape-route-helpers (~> 2.0.0)
+ grape-route-helpers (~> 2.1.0)
+ grape_logging (~> 1.6)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
@@ -1004,12 +1047,12 @@ DEPENDENCIES
html2text
httparty (~> 0.13.3)
influxdb (~> 0.2)
- jira-ruby (~> 1.1.2)
+ jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0)
json-schema (~> 2.6.2)
jwt (~> 1.5.6)
- kaminari (~> 0.17.0)
+ kaminari (~> 1.0)
knapsack (~> 1.11.0)
kubeclient (~> 2.2.0)
letter_opener_web (~> 1.3.0)
@@ -1024,7 +1067,7 @@ DEPENDENCIES
mysql2 (~> 0.4.5)
net-ldap
net-ssh (~> 4.1.0)
- nokogiri (~> 1.6.7, >= 1.6.7.2)
+ nokogiri (~> 1.8.0)
oauth2 (~> 1.4)
octokit (~> 4.6.2)
oj (~> 2.17.4)
@@ -1032,11 +1075,11 @@ DEPENDENCIES
omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.1)
omniauth-azure-oauth2 (~> 0.0.6)
- omniauth-cas3 (~> 1.1.2)
+ omniauth-cas3 (~> 1.1.4)
omniauth-facebook (~> 4.0.0)
omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.2)
- omniauth-google-oauth2 (~> 0.4.1)
+ omniauth-google-oauth2 (~> 0.5.2)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.7.0)
@@ -1057,7 +1100,7 @@ DEPENDENCIES
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.7.0.beta11)
+ prometheus-client-mmap (~> 0.7.0.beta14)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
@@ -1070,7 +1113,7 @@ DEPENDENCIES
rainbow (~> 2.2)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
- rbnacl (~> 3.2)
+ rbnacl (~> 4.0)
rbnacl-libsodium
rdoc (~> 4.2)
re2 (~> 1.1.1)
@@ -1083,11 +1126,13 @@ DEPENDENCIES
responders (~> 2.0)
rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7)
+ rspec-parameterized
rspec-rails (~> 3.6.0)
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
rubocop (~> 0.49.1)
+ rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.15.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
@@ -1106,6 +1151,7 @@ DEPENDENCIES
sidekiq (~> 5.0)
sidekiq-cron (~> 0.6.0)
sidekiq-limit_fetch (~> 3.4)
+ simple_po_parser (~> 1.1.2)
simplecov (~> 0.14.0)
slack-notifier (~> 1.5.1)
spinach-rails (~> 0.2.1)
@@ -1121,10 +1167,9 @@ DEPENDENCIES
thin (~> 1.7.0)
timecop (~> 0.8.0)
toml-rb (~> 0.3.15)
- truncato (~> 0.7.8)
+ truncato (~> 0.7.9)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
- underscore-rails (~> 1.8.0)
unf (~> 0.1.4)
unicorn (~> 5.1.0)
unicorn-worker-killer (~> 0.4.4)
@@ -1137,4 +1182,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.15.3
+ 1.15.4
diff --git a/PROCESS.md b/PROCESS.md
index 2b3d142bf77..ed4e84dd0b6 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -119,6 +119,12 @@ only be left until after the freeze if:
are aware of it.
* It is in the correct milestone, with the ~Deliverable label.
+If a merge request is not ready, but the developers and Product Manager
+responsible for the feature think it is essential that it is in the release,
+they can [ask for an exception](#asking-for-an-exception) in advance. This is
+preferable to merging something that we are not confident in, but should still
+be a rare case: most features can be allowed to slip a release.
+
All Community Edition merge requests from GitLab team members merged on the
freeze date (the 7th) should have a corresponding Enterprise Edition merge
request, even if there are no conflicts. This is to reduce the size of the
@@ -128,11 +134,30 @@ information, see
### After the 7th
-Once the stable branch is frozen, only fixes for [regressions](#regressions)
-and security issues will be cherry-picked into the stable branch.
-Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
-These fixes will be shipped in the next RC for that release if it is before the 22nd.
-If the fixes are are completed on or after the 22nd, they will be shipped in a patch for that release.
+Once the stable branch is frozen, the only MRs that can be cherry-picked into
+the stable branch are:
+
+* Fixes for [regressions](#regressions)
+* Fixes for security issues
+* New or updated translations (as long as they do not touch application code)
+
+During the feature freeze all merge requests that are meant to go into the upcoming
+release should have the correct milestone assigned _and_ have the label
+~"Pick into Stable" set, so that release managers can find and pick them.
+Merge requests without a milestone and this label will
+not be merged into any stable branches.
+
+Fixes marked like this will be shipped in the next RC for that release. Once
+the final RC has been prepared ready for release on the 22nd, further fixes
+marked ~"Pick into Stable" will go into a patch for that release.
+
+If a merge request is to be picked into more than one release it will also need
+the ~"Pick into Backports" label set to remind the release manager to change
+the milestone after cherry-picking. As before, it should still have the
+~"Pick into Stable" label and the milestone of the highest release it will be
+picked into.
+
+### Asking for an exception
If you think a merge request should go into an RC or patch even though it does not meet these requirements,
you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer:
@@ -152,11 +177,7 @@ When in doubt, we err on the side of _not_ cherry-picking.
For example, it is likely that an exception will be made for a trivial 1-5 line performance improvement
(e.g. adding a database index or adding `includes` to a query), but not for a new feature, no matter how relatively small or thoroughly tested.
-During the feature freeze all merge requests that are meant to go into the upcoming
-release should have the correct milestone assigned _and_ have the label
-~"Pick into Stable" set, so that release managers can find and pick them.
-Merge requests without a milestone and this label will
-not be merged into any stable branches.
+All MRs which have had exceptions granted must be merged by the 15th.
### Regressions
@@ -178,26 +199,8 @@ available in the package repositories.
## Release retrospective and kickoff
-### Retrospective
-
-After each release, we have a retrospective call where we discuss what went well,
-what went wrong, and what we can improve for the next release. The
-[retrospective notes] are public and you are invited to comment on them.
-If you're interested, you can even join the
-[retrospective call][retro-kickoff-call], on the first working day after the
-22nd at 6pm CET / 9am PST.
-
-### Kickoff
-
-Before working on the next release, we have a
-kickoff call to explain what we expect to ship in the next release. The
-[kickoff notes] are public and you are invited to comment on them.
-If you're interested, you can even join the [kickoff call][retro-kickoff-call],
-on the first working day after the 7th at 6pm CET / 9am PST..
-
-[retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing
-[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing
-[retro-kickoff-call]: https://gitlab.zoom.us/j/918821206
+- [Retrospective](https://about.gitlab.com/handbook/engineering/workflow/#retrospective)
+- [Kickoff](https://about.gitlab.com/handbook/engineering/workflow/#kickoff)
## Copy & paste responses
diff --git a/README.md b/README.md
index 9309922ae39..9ead6d51c5d 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
+[![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.svg)](https://gemnasium.com/gitlabhq/gitlabhq)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
diff --git a/VERSION b/VERSION
index 027fe8dd2cf..ddadd9f9c5a 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-9.5.0-pre
+9.6.0-pre
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 76b724e1bcb..38d1effc77c 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -5,8 +5,9 @@ const Api = {
groupPath: '/api/:version/groups/:id.json',
namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json',
- projectsPath: '/api/:version/projects.json?simple=true',
- labelsPath: '/:namespace_path/:project_path/labels',
+ projectsPath: '/api/:version/projects.json',
+ projectLabelsPath: '/:namespace_path/:project_path/labels',
+ groupLabelsPath: '/groups/:namespace_path/labels',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key',
@@ -55,22 +56,35 @@ const Api = {
// Return projects list. Filtered by query
projects(query, options, callback) {
const url = Api.buildUrl(Api.projectsPath);
+ const defaults = {
+ search: query,
+ per_page: 20,
+ simple: true,
+ };
+
+ if (gon.current_user_id) {
+ defaults.membership = true;
+ }
+
return $.ajax({
url,
- data: Object.assign({
- search: query,
- per_page: 20,
- membership: true,
- }, options),
+ data: Object.assign(defaults, options),
dataType: 'json',
})
.done(projects => callback(projects));
},
newLabel(namespacePath, projectPath, data, callback) {
- const url = Api.buildUrl(Api.labelsPath)
- .replace(':namespace_path', namespacePath)
- .replace(':project_path', projectPath);
+ let url;
+
+ if (projectPath) {
+ url = Api.buildUrl(Api.projectLabelsPath)
+ .replace(':namespace_path', namespacePath)
+ .replace(':project_path', projectPath);
+ } else {
+ url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
+ }
+
return $.ajax({
url,
type: 'POST',
@@ -96,19 +110,17 @@ const Api = {
.done(projects => callback(projects));
},
- commitMultiple(id, data, callback) {
+ commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath)
.replace(':id', id);
- return $.ajax({
+ return this.wrapAjaxCall({
url,
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(data),
dataType: 'json',
- })
- .done(commitData => callback(commitData))
- .fail(message => callback(message.responseJSON));
+ });
},
// Return text for a specific license
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index cfab6c40b34..4d2d4db7c0e 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -2,17 +2,17 @@
import AccessorUtilities from './lib/utils/accessor';
window.Autosave = (function() {
- function Autosave(field, key) {
+ function Autosave(field, key, resource) {
this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
-
+ this.resource = resource;
if (key.join != null) {
- key = key.join("/");
+ key = key.join('/');
}
- this.key = "autosave/" + key;
- this.field.data("autosave", this);
+ this.key = 'autosave/' + key;
+ this.field.data('autosave', this);
this.restore();
- this.field.on("input", (function(_this) {
+ this.field.on('input', (function(_this) {
return function() {
return _this.save();
};
@@ -29,7 +29,17 @@ window.Autosave = (function() {
if ((text != null ? text.length : void 0) > 0) {
this.field.val(text);
}
- return this.field.trigger("input");
+ if (!this.resource && this.resource !== 'issue') {
+ this.field.trigger('input');
+ } else {
+ // v-model does not update with jQuery trigger
+ // https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
+ const event = new Event('change', { bubbles: true, cancelable: false });
+ const field = this.field.get(0);
+ if (field) {
+ field.dispatchEvent(event);
+ }
+ }
};
Autosave.prototype.save = function() {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 097f79a250a..22fa1f2a609 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -109,6 +109,7 @@ class AwardsHandler {
}
$thumbsBtn.toggleClass('disabled', $userAuthored);
+ $thumbsBtn.prop('disabled', $userAuthored);
}
// Create the emoji menu with the first category of emojis.
@@ -234,14 +235,33 @@ class AwardsHandler {
}
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
+ const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length;
+
+ if (gl.utils.isInIssuePage() && !isMainAwardsBlock) {
+ const id = votesBlock.attr('id').replace('note_', '');
+
+ $('.emoji-menu').removeClass('is-visible');
+ $('.js-add-award.is-active').removeClass('is-active');
+ const toggleAwardEvent = new CustomEvent('toggleAward', {
+ detail: {
+ awardName: emoji,
+ noteId: id,
+ },
+ });
+
+ document.querySelector('.js-vue-notes-event').dispatchEvent(toggleAwardEvent);
+ }
+
const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
+
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
return typeof callback === 'function' ? callback() : undefined;
});
+
$('.emoji-menu').removeClass('is-visible');
- $('.js-add-award.is-active').removeClass('is-active');
+ return $('.js-add-award.is-active').removeClass('is-active');
}
addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
@@ -268,6 +288,14 @@ class AwardsHandler {
}
getVotesBlock() {
+ if (gl.utils.isInIssuePage()) {
+ const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
+
+ if ($el.length) {
+ return $el;
+ }
+ }
+
const currentBlock = $('.js-awards-block.current');
let resultantVotesBlock = currentBlock;
if (currentBlock.length === 0) {
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index bc693616460..79702c54852 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -44,7 +44,10 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
if (!$submitButton.attr('disabled')) {
$submitButton.trigger('click', [e]);
- $submitButton.disable();
+
+ if (!gl.utils.isInIssuePage()) {
+ $submitButton.disable();
+ }
}
});
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index dc636050221..26d3419a162 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,9 +1,24 @@
/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
/* global Dropzone */
+import '../lib/utils/url_utility';
+import { HIDDEN_CLASS } from '../lib/utils/constants';
+
+function toggleLoading($el, $icon, loading) {
+ if (loading) {
+ $el.disable();
+ $icon.removeClass(HIDDEN_CLASS);
+ } else {
+ $el.enable();
+ $icon.addClass(HIDDEN_CLASS);
+ }
+}
export default class BlobFileDropzone {
constructor(form, method) {
const formDropzone = form.find('.dropzone');
+ const submitButton = form.find('#submit-all');
+ const submitButtonLoadingIcon = submitButton.find('.js-loading-icon');
+ const dropzoneMessage = form.find('.dz-message');
Dropzone.autoDiscover = false;
const dropzone = formDropzone.dropzone({
@@ -26,12 +41,20 @@ export default class BlobFileDropzone {
},
init: function () {
this.on('addedfile', function () {
+ toggleLoading(submitButton, submitButtonLoadingIcon, false);
+ dropzoneMessage.addClass(HIDDEN_CLASS);
$('.dropzone-alerts').html('').hide();
});
+ this.on('removedfile', function () {
+ toggleLoading(submitButton, submitButtonLoadingIcon, false);
+ dropzoneMessage.removeClass(HIDDEN_CLASS);
+ });
this.on('success', function (header, response) {
- window.location.href = response.filePath;
+ $('#modal-upload-blob').modal('hide');
+ window.gl.utils.visitUrl(response.filePath);
});
this.on('maxfilesexceeded', function (file) {
+ dropzoneMessage.addClass(HIDDEN_CLASS);
this.removeFile(file);
});
this.on('sending', function (file, xhr, formData) {
@@ -48,14 +71,15 @@ export default class BlobFileDropzone {
},
});
- const submitButton = form.find('#submit-all')[0];
- submitButton.addEventListener('click', function (e) {
+ submitButton.on('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
// eslint-disable-next-line no-alert
alert('Please select a file');
+ return false;
}
+ toggleLoading(submitButton, submitButtonLoadingIcon, true);
dropzone[0].dropzone.processQueue();
return false;
});
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index 89c14180149..ea00efe4b46 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -53,7 +53,8 @@ $(() => {
data: {
state: Store.state,
loading: true,
- endpoint: $boardApp.dataset.endpoint,
+ boardsEndpoint: $boardApp.dataset.boardsEndpoint,
+ listsEndpoint: $boardApp.dataset.listsEndpoint,
boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true',
issueLinkBase: $boardApp.dataset.issueLinkBase,
@@ -68,7 +69,13 @@ $(() => {
},
},
created () {
- gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
+ gl.boardService = new BoardService({
+ boardsEndpoint: this.boardsEndpoint,
+ listsEndpoint: this.listsEndpoint,
+ bulkUpdatePath: this.bulkUpdatePath,
+ boardId: this.boardId,
+ });
+ Store.rootPath = this.boardsEndpoint;
this.filterManager = new FilteredSearchBoards(Store.filter, true);
this.filterManager.setup();
@@ -112,19 +119,21 @@ $(() => {
gl.IssueBoardsSearch = new Vue({
el: document.getElementById('js-add-list'),
data: {
- filters: Store.state.filters
+ filters: Store.state.filters,
},
mounted () {
gl.issueBoards.newListDropdownInit();
- }
+ },
});
gl.IssueBoardsModalAddBtn = new Vue({
mixins: [gl.issueBoards.ModalMixins],
el: document.getElementById('js-add-issues-btn'),
- data: {
- modal: ModalStore.store,
- store: Store.state,
+ data() {
+ return {
+ modal: ModalStore.store,
+ store: Store.state,
+ };
},
watch: {
disabled() {
@@ -133,6 +142,9 @@ $(() => {
},
computed: {
disabled() {
+ if (!this.store) {
+ return true;
+ }
return !this.store.lists.filter(list => !list.preset).length;
},
tooltipTitle() {
@@ -145,7 +157,7 @@ $(() => {
},
methods: {
updateTooltip() {
- const $tooltip = $(this.$el);
+ const $tooltip = $(this.$refs.addIssuesButton);
this.$nextTick(() => {
if (this.disabled) {
@@ -165,16 +177,19 @@ $(() => {
this.updateTooltip();
},
template: `
- <button
- class="btn btn-create pull-right prepend-left-10"
- type="button"
- data-placement="bottom"
- :class="{ 'disabled': disabled }"
- :title="tooltipTitle"
- :aria-disabled="disabled"
- @click="openModal">
- Add issues
- </button>
+ <div class="board-extra-actions">
+ <button
+ class="btn btn-create prepend-left-10"
+ type="button"
+ data-placement="bottom"
+ ref="addIssuesButton"
+ :class="{ 'disabled': disabled }"
+ :title="tooltipTitle"
+ :aria-disabled="disabled"
+ @click="openModal">
+ Add issues
+ </button>
+ </div>
`,
});
});
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index bebca17fb1e..6159680f1e6 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -77,7 +77,7 @@ export default {
this.showIssueForm = !this.showIssueForm;
},
onScroll() {
- if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
+ if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
this.loadNextPage();
}
},
@@ -165,11 +165,9 @@ export default {
v-if="loading">
<loading-icon />
</div>
- <transition name="slide-down">
- <board-new-issue
- :list="list"
- v-if="list.type !== 'closed' && showIssueForm"/>
- </transition>
+ <board-new-issue
+ :list="list"
+ v-if="list.type !== 'closed' && showIssueForm"/>
<ul
class="board-list"
v-show="!loading"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js
index 4af8b0c7713..541b8049855 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js
+++ b/app/assets/javascripts/boards/components/board_new_issue.js
@@ -6,7 +6,10 @@ const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardNewIssue',
props: {
- list: Object,
+ list: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index d3de1830895..bf474879024 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -64,10 +64,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return this.issue.assignees.length > this.numberOverLimit;
},
cardUrl() {
- return `${this.issueLinkBase}/${this.issue.id}`;
+ return `${this.issueLinkBase}/${this.issue.iid}`;
},
issueId() {
- return `#${this.issue.id}`;
+ if (this.issue.iid) {
+ return `#${this.issue.iid}`;
+ }
+ return false;
},
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
@@ -97,7 +100,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return `Avatar for ${assignee.name}`;
},
showLabel(label) {
- if (!this.list || !label) return true;
+ if (!label.id) return false;
return true;
},
filterByLabel(label, e) {
@@ -143,7 +146,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({
:title="issue.title">{{ issue.title }}</a>
<span
class="card-number"
- v-if="issue.id"
+ v-if="issueId"
>
{{ issueId }}
</span>
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index 478a1335b2b..a656f0546c0 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -29,7 +29,7 @@ gl.issueBoards.ModalFooter = Vue.extend({
const firstListIndex = 1;
const list = this.modal.selectedList || this.state.lists[firstListIndex];
const selectedIssues = ModalStore.getSelectedIssues();
- const issueIds = selectedIssues.map(issue => issue.globalId);
+ const issueIds = selectedIssues.map(issue => issue.id);
// Post the data to the backend
gl.boardService.bulkUpdate(issueIds, {
diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js
index 363269c0d5d..b4a45feee4d 100644
--- a/app/assets/javascripts/boards/components/modal/list.js
+++ b/app/assets/javascripts/boards/components/modal/list.js
@@ -1,7 +1,7 @@
/* global ListIssue */
-/* global bp */
import Vue from 'vue';
+import bp from '../../../breakpoints';
const ModalStore = gl.issueBoards.ModalStore;
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 72bb9e10fbc..d7f203b3f96 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -27,7 +27,7 @@ gl.issueBoards.newListDropdownInit = () => {
$this.glDropdown({
data(term, callback) {
- $.get($this.attr('data-labels'))
+ $.get($this.attr('data-list-labels-path'))
.then((resp) => {
callback(resp);
});
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index 6a900d4abd0..1e623cf58b7 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -18,17 +18,33 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({
type: Object,
required: true,
},
+ issueUpdate: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ updateUrl() {
+ return this.issueUpdate;
+ },
},
methods: {
removeIssue() {
const issue = this.issue;
const lists = issue.getLists();
- const labelIds = lists.map(list => list.label.id);
-
- // Post the remove data
- gl.boardService.bulkUpdate([issue.globalId], {
- remove_label_ids: labelIds,
- }).catch(() => {
+ const listLabelIds = lists.map(list => list.label.id);
+ let labelIds = this.issue.labels
+ .map(label => label.id)
+ .filter(id => !listLabelIds.includes(id));
+ if (labelIds.length === 0) {
+ labelIds = [''];
+ }
+ const data = {
+ issue: {
+ label_ids: labelIds,
+ },
+ };
+ Vue.http.patch(this.updateUrl, data).catch(() => {
new Flash('Failed to remove issue from board, please try again.', 'alert');
lists.forEach((list) => {
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 6c2d8a3781b..407db176446 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -7,8 +7,8 @@ import Vue from 'vue';
class ListIssue {
constructor (obj, defaultAvatar) {
- this.globalId = obj.id;
- this.id = obj.iid;
+ this.id = obj.id;
+ this.iid = obj.iid;
this.title = obj.title;
this.confidential = obj.confidential;
this.dueDate = obj.due_date;
diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/boards/models/label.js
index 9af88d167d6..98c1ec014c4 100644
--- a/app/assets/javascripts/boards/models/label.js
+++ b/app/assets/javascripts/boards/models/label.js
@@ -4,6 +4,7 @@ class ListLabel {
constructor (obj) {
this.id = obj.id;
this.title = obj.title;
+ this.type = obj.type;
this.color = obj.color;
this.textColor = obj.text_color;
this.description = obj.description;
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 08f7c5ddcd2..df2809e1805 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -110,11 +110,13 @@ class List {
return gl.boardService.newIssue(this.id, issue)
.then(resp => resp.json())
.then((data) => {
- issue.id = data.iid;
+ issue.id = data.id;
+ issue.iid = data.iid;
+ issue.project = data.project;
if (this.issuesSize > 1) {
- const moveBeforeIid = this.issues[1].id;
- gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
+ const moveBeforeId = this.issues[1].id;
+ gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeId);
}
});
}
@@ -126,19 +128,19 @@ class List {
}
addIssue (issue, listFrom, newIndex) {
- let moveBeforeIid = null;
- let moveAfterIid = null;
+ let moveBeforeId = null;
+ let moveAfterId = null;
if (!this.findIssue(issue.id)) {
if (newIndex !== undefined) {
this.issues.splice(newIndex, 0, issue);
if (this.issues[newIndex - 1]) {
- moveBeforeIid = this.issues[newIndex - 1].id;
+ moveBeforeId = this.issues[newIndex - 1].id;
}
if (this.issues[newIndex + 1]) {
- moveAfterIid = this.issues[newIndex + 1].id;
+ moveAfterId = this.issues[newIndex + 1].id;
}
} else {
this.issues.push(issue);
@@ -151,30 +153,30 @@ class List {
if (listFrom) {
this.issuesSize += 1;
- this.updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid);
+ this.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId);
}
}
}
- moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) {
+ moveIssue (issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
this.issues.splice(oldIndex, 1);
this.issues.splice(newIndex, 0, issue);
- gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid)
+ gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId)
.catch(() => {
// TODO: handle request error
});
}
- updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
- gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid)
+ updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
+ gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
.catch(() => {
// TODO: handle request error
});
}
findIssue (id) {
- return this.issues.filter(issue => issue.id === id)[0];
+ return this.issues.find(issue => issue.id === id);
}
removeIssue (removeIssue) {
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 3742507b236..38eea38f949 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -3,21 +3,21 @@
import Vue from 'vue';
class BoardService {
- constructor (root, bulkUpdatePath, boardId) {
- this.boards = Vue.resource(`${root}{/id}.json`, {}, {
+ constructor ({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId }) {
+ this.boards = Vue.resource(`${boardsEndpoint}{/id}.json`, {}, {
issues: {
method: 'GET',
- url: `${root}/${boardId}/issues.json`
+ url: `${gon.relative_url_root}/boards/${boardId}/issues.json`,
}
});
- this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
+ this.lists = Vue.resource(`${listsEndpoint}{/id}`, {}, {
generate: {
method: 'POST',
- url: `${root}/${boardId}/lists/generate.json`
+ url: `${listsEndpoint}/generate.json`
}
});
- this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {});
- this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, {
+ this.issue = Vue.resource(`${gon.relative_url_root}/boards/${boardId}/issues{/id}`, {});
+ this.issues = Vue.resource(`${listsEndpoint}{/id}/issues`, {}, {
bulkUpdate: {
method: 'POST',
url: bulkUpdatePath,
@@ -60,12 +60,12 @@ class BoardService {
return this.issues.get(data);
}
- moveIssue (id, from_list_id = null, to_list_id = null, move_before_iid = null, move_after_iid = null) {
+ moveIssue (id, from_list_id = null, to_list_id = null, move_before_id = null, move_after_id = null) {
return this.issue.update({ id }, {
from_list_id,
to_list_id,
- move_before_iid,
- move_after_iid,
+ move_before_id,
+ move_after_id,
});
}
diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js
new file mode 100644
index 00000000000..10fbcfe96cf
--- /dev/null
+++ b/app/assets/javascripts/breadcrumb.js
@@ -0,0 +1,28 @@
+export const addTooltipToEl = (el) => {
+ const textEl = el.querySelector('.js-breadcrumb-item-text');
+
+ if (textEl && textEl.scrollWidth > textEl.offsetWidth) {
+ el.setAttribute('title', el.textContent);
+ el.setAttribute('data-container', 'body');
+ el.classList.add('has-tooltip');
+ }
+};
+
+export default () => {
+ const breadcrumbs = document.querySelector('.js-breadcrumbs-list');
+
+ if (breadcrumbs) {
+ const topLevelLinks = [...breadcrumbs.children].filter(el => !el.classList.contains('dropdown'))
+ .map(el => el.querySelector('a'))
+ .filter(el => el);
+ const $expander = $('.js-breadcrumbs-collapsed-expander');
+
+ topLevelLinks.forEach(el => addTooltipToEl(el));
+
+ $expander.closest('.dropdown')
+ .on('show.bs.dropdown hide.bs.dropdown', (e) => {
+ $('.js-breadcrumbs-collapsed-expander', e.currentTarget).toggleClass('open')
+ .tooltip('hide');
+ });
+ }
+};
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index 2c1f988d987..7951348d8b2 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,66 +1,19 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, no-return-assign, new-parens, no-param-reassign, max-len */
+export const breakpoints = {
+ lg: 1200,
+ md: 992,
+ sm: 768,
+ xs: 0,
+};
-var Breakpoints = (function() {
- var BreakpointInstance, instance;
+const BreakpointInstance = {
+ windowWidth: () => window.innerWidth,
+ getBreakpointSize() {
+ const windowWidth = this.windowWidth();
- function Breakpoints() {}
+ const breakpoint = Object.keys(breakpoints).find(key => windowWidth > breakpoints[key]);
- instance = null;
+ return breakpoint;
+ },
+};
- BreakpointInstance = (function() {
- var BREAKPOINTS;
-
- BREAKPOINTS = ["xs", "sm", "md", "lg"];
-
- function BreakpointInstance() {
- this.setup();
- }
-
- BreakpointInstance.prototype.setup = function() {
- var allDeviceSelector, els;
- allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
- return ".device-" + breakpoint;
- });
- if ($(allDeviceSelector.join(",")).length) {
- return;
- }
- // Create all the elements
- els = $.map(BREAKPOINTS, function(breakpoint) {
- return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
- });
- return $("body").append(els.join(''));
- };
-
- BreakpointInstance.prototype.visibleDevice = function() {
- var allDeviceSelector;
- allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
- return ".device-" + breakpoint;
- });
- return $(allDeviceSelector.join(",")).filter(":visible");
- };
-
- BreakpointInstance.prototype.getBreakpointSize = function() {
- var $visibleDevice;
- $visibleDevice = this.visibleDevice;
- // TODO: Consider refactoring in light of turbolinks removal.
- // the page refreshed via turbolinks
- if (!$visibleDevice().length) {
- this.setup();
- }
- $visibleDevice = this.visibleDevice();
- return $visibleDevice.attr("class").split("visible-")[1];
- };
-
- return BreakpointInstance;
- })();
-
- Breakpoints.get = function() {
- return instance != null ? instance : instance = new BreakpointInstance;
- };
-
- return Breakpoints;
-})();
-
-$(() => { window.bp = Breakpoints.get(); });
-
-window.Breakpoints = Breakpoints;
+export default BreakpointInstance;
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 940326dcd33..ae1a23132a7 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,8 +1,7 @@
/* eslint-disable func-names, wrap-iife, no-use-before-define,
consistent-return, prefer-rest-params */
-/* global Breakpoints */
-
import _ from 'underscore';
+import bp from './breakpoints';
import { bytesToKiB } from './lib/utils/number_utils';
window.Build = (function () {
@@ -34,8 +33,6 @@ window.Build = (function () {
this.$scrollBottomBtn = $('.js-scroll-down');
clearTimeout(Build.timeout);
- // Init breakpoint checker
- this.bp = Breakpoints.get();
this.initSidebar();
this.populateJobs(this.buildStage);
@@ -230,7 +227,7 @@ window.Build = (function () {
};
Build.prototype.shouldHideSidebarForViewport = function () {
- const bootstrapBreakpoint = this.bp.getBreakpointSize();
+ const bootstrapBreakpoint = bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
};
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 687f09882a7..16c5d0fa344 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -35,6 +35,7 @@ document.addEventListener('DOMContentLoaded', () => {
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
+ autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
},
}).$mount();
pipelineTableViewEl.appendChild(table.$el);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index dd751ec97a8..c931e1e0ea5 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -13,6 +13,10 @@
type: String,
required: true,
},
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
+ },
},
mixins: [
pipelinesMixin,
@@ -95,6 +99,7 @@
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
+ :auto-devops-help-path="autoDevopsHelpPath"
/>
</div>
</div>
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 2b0bf49cf92..047544b1762 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -17,7 +17,7 @@ window.CommitsList = (function() {
}
});
- Pager.init(limit, false, false, this.processCommits);
+ Pager.init(parseInt(limit, 10), false, false, this.processCommits);
this.content = $("#commits-list");
this.searchField = $("#commits-search");
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
index 6db8b3afbef..768453b28f1 100644
--- a/app/assets/javascripts/commons/index.js
+++ b/app/assets/javascripts/commons/index.js
@@ -2,3 +2,4 @@ import 'underscore';
import './polyfills';
import './jquery';
import './bootstrap';
+import './vue';
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index bc3e741f524..b78089525cc 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -12,3 +12,4 @@ import 'core-js/fn/symbol';
// Browser polyfills
import './polyfills/custom_event';
import './polyfills/element';
+import './polyfills/nodelist';
diff --git a/app/assets/javascripts/commons/polyfills/nodelist.js b/app/assets/javascripts/commons/polyfills/nodelist.js
new file mode 100644
index 00000000000..3772c94b900
--- /dev/null
+++ b/app/assets/javascripts/commons/polyfills/nodelist.js
@@ -0,0 +1,7 @@
+if (window.NodeList && !NodeList.prototype.forEach) {
+ NodeList.prototype.forEach = function forEach(callback, thisArg = window) {
+ for (let i = 0; i < this.length; i += 1) {
+ callback.call(thisArg, this[i], i, this);
+ }
+ };
+}
diff --git a/app/assets/javascripts/commons/vue.js b/app/assets/javascripts/commons/vue.js
new file mode 100644
index 00000000000..8b62d78c043
--- /dev/null
+++ b/app/assets/javascripts/commons/vue.js
@@ -0,0 +1,5 @@
+import Vue from 'vue';
+
+if (process.env.NODE_ENV !== 'production') {
+ Vue.config.productionTip = false;
+}
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index ab9a8e43dd1..1f3c7e1772d 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -29,12 +29,14 @@ showTooltip = function(target, title) {
var $target = $(target);
var originalTitle = $target.data('original-title');
- $target
- .attr('title', 'Copied')
- .tooltip('fixTitle')
- .tooltip('show')
- .attr('title', originalTitle)
- .tooltip('fixTitle');
+ if (!$target.data('hideTooltip')) {
+ $target
+ .attr('title', 'Copied')
+ .tooltip('fixTitle')
+ .tooltip('show')
+ .attr('title', originalTitle)
+ .tooltip('fixTitle');
+ }
};
$(function() {
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index c37249c060a..06ce84d7599 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -21,11 +21,13 @@ const DiffNoteAvatars = Vue.extend({
},
template: `
<div class="diff-comment-avatar-holders"
+ :class="discussionClassName"
v-show="notesCount !== 0">
<div v-if="!isVisible">
<!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image
v-for="note in notesSubset"
+ :key="note.id"
class="diff-comment-avatar js-diff-comment-avatar"
@click.native="clickedAvatar($event)"
:img-src="note.authorAvatar"
@@ -68,7 +70,8 @@ const DiffNoteAvatars = Vue.extend({
});
});
},
- destroyed() {
+ beforeDestroy() {
+ this.addNoCommentClass();
$(document).off('toggle.comments');
},
watch: {
@@ -85,6 +88,9 @@ const DiffNoteAvatars = Vue.extend({
},
},
computed: {
+ discussionClassName() {
+ return `js-diff-avatars-${this.discussionId}`;
+ },
notesSubset() {
let notes = [];
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index a2d33b0936e..0863c3406bd 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -32,6 +32,10 @@ $(() => {
const tmpApp = new tmp().$mount();
$(this).replaceWith(tmpApp.$el);
+ $(tmpApp.$el).one('remove.vue', () => {
+ tmpApp.$destroy();
+ tmpApp.$el.remove();
+ });
});
const $components = $(COMPONENT_SELECTOR).filter(function () {
@@ -42,6 +46,10 @@ $(() => {
$components.each(function () {
const $this = $(this);
const noteId = $this.attr(':note-id');
+ const discussionId = $this.attr(':discussion-id');
+
+ if ($this.is('comment-and-resolve-btn') && !discussionId) return;
+
const tmp = Vue.extend({
template: $this.get(0).outerHTML
});
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 7cc7636cca3..f3b537c83e2 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -41,7 +41,6 @@ import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
import DeleteModal from './branches/branches_delete_modal';
import Group from './group';
-import GroupName from './group_name';
import GroupsList from './groups_list';
import ProjectsList from './projects_list';
import setupProjectEdit from './project_edit';
@@ -74,8 +73,10 @@ import PerformanceBar from './performance_bar';
import initNotes from './init_notes';
import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar';
+import initProjectVisibilitySelector from './project_visibility';
import GpgBadges from './gpg_badges';
import UserFeatureHelper from './helpers/user_feature_helper';
+import initChangesDropdown from './init_changes_dropdown';
(function() {
var Dispatcher;
@@ -97,7 +98,7 @@ import UserFeatureHelper from './helpers/user_feature_helper';
path = page.split(':');
shortcut_handler = null;
- $('.js-gfm-input').each((i, el) => {
+ $('.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);
gfm.setup($(el), {
@@ -159,6 +160,9 @@ import UserFeatureHelper from './helpers/user_feature_helper';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
+ if (page === 'projects:merge_requests:index') {
+ new UserCallout({ setCalloutPerProject: true });
+ }
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
IssuableIndex.init(pagePrefix);
@@ -170,7 +174,6 @@ import UserFeatureHelper from './helpers/user_feature_helper';
shortcut_handler = new ShortcutsIssuable();
new ZenMode();
initIssuableSidebar();
- initNotes();
break;
case 'dashboard:milestones:index':
new ProjectSelect();
@@ -183,13 +186,13 @@ import UserFeatureHelper from './helpers/user_feature_helper';
break;
case 'dashboard:issues':
case 'dashboard:merge_requests':
- case 'groups:merge_requests':
new ProjectSelect();
initLegacyFilters();
break;
case 'groups:issues':
+ case 'groups:merge_requests':
if (filteredSearchEnabled) {
- const filteredSearchManager = new gl.FilteredSearchManager('issues');
+ const filteredSearchManager = new gl.FilteredSearchManager(page === 'groups:issues' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
new ProjectSelect();
@@ -228,6 +231,7 @@ import UserFeatureHelper from './helpers/user_feature_helper';
break;
case 'projects:compare:show':
new gl.Diff();
+ initChangesDropdown();
break;
case 'projects:branches:new':
case 'projects:branches:create':
@@ -320,6 +324,7 @@ import UserFeatureHelper from './helpers/user_feature_helper';
container: '.js-commit-pipeline-graph',
}).bindEvents();
initNotes();
+ initChangesDropdown();
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
break;
case 'projects:commit:pipelines':
@@ -340,10 +345,14 @@ import UserFeatureHelper from './helpers/user_feature_helper';
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
+ new UserCallout({ setCalloutPerProject: true });
if ($('#tree-slider').length) new TreeView();
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);
+ });
break;
case 'projects:edit':
setupProjectEdit();
@@ -356,6 +365,9 @@ import UserFeatureHelper from './helpers/user_feature_helper';
case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form'));
break;
+ case 'projects:pipelines:index':
+ new UserCallout({ setCalloutPerProject: true });
+ break;
case 'projects:pipelines:builds':
case 'projects:pipelines:failures':
case 'projects:pipelines:show':
@@ -408,11 +420,12 @@ import UserFeatureHelper from './helpers/user_feature_helper';
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
- if (UserFeatureHelper.isNewRepo()) break;
+ if (UserFeatureHelper.isNewRepoEnabled()) break;
new TreeView();
new BlobViewer();
new NewCommitForm($('.js-create-dir-form'));
+ new UserCallout({ setCalloutPerProject: true });
$('#tree-slider').waitForImages(function() {
gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
@@ -428,7 +441,7 @@ import UserFeatureHelper from './helpers/user_feature_helper';
shortcut_handler = true;
break;
case 'projects:blob:show':
- if (UserFeatureHelper.isNewRepo()) break;
+ if (UserFeatureHelper.isNewRepoEnabled()) break;
new BlobViewer();
initBlob();
break;
@@ -483,6 +496,8 @@ import UserFeatureHelper from './helpers/user_feature_helper';
initSettingsPanels();
break;
case 'projects:settings:ci_cd:show':
+ // Initialize expandable settings panels
+ initSettingsPanels();
case 'groups:settings:ci_cd:show':
new gl.ProjectVariables();
break;
@@ -548,9 +563,6 @@ import UserFeatureHelper from './helpers/user_feature_helper';
case 'root':
new UserCallout();
break;
- case 'groups':
- new GroupName();
- break;
case 'profiles':
new NotificationsForm();
new NotificationsDropdown();
@@ -558,7 +570,6 @@ import UserFeatureHelper from './helpers/user_feature_helper';
case 'projects':
new Project();
new ProjectAvatar();
- new GroupName();
switch (path[1]) {
case 'compare':
new CompareAutocomplete();
@@ -566,9 +577,13 @@ import UserFeatureHelper from './helpers/user_feature_helper';
case 'edit':
shortcut_handler = new ShortcutsNavigation();
new ProjectNew();
+ import(/* webpackChunkName: 'project_permissions' */ './projects/permissions')
+ .then(permissions => permissions.default())
+ .catch(() => {});
break;
case 'new':
new ProjectNew();
+ initProjectVisibilitySelector();
break;
case 'show':
new Star();
@@ -638,7 +653,7 @@ import UserFeatureHelper from './helpers/user_feature_helper';
return Dispatcher;
})();
- $(function() {
+ $(window).on('load', function() {
new Dispatcher();
});
}).call(window);
diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js
index 70cd337fb8a..3901bb177fe 100644
--- a/app/assets/javascripts/droplab/drop_down.js
+++ b/app/assets/javascripts/droplab/drop_down.js
@@ -85,6 +85,13 @@ class DropDown {
const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list;
renderableList.innerHTML = children.join('');
+
+ const listEvent = new CustomEvent('render.dl', {
+ detail: {
+ list: this,
+ },
+ });
+ this.list.dispatchEvent(listEvent);
}
renderChildren(data) {
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 6d19a6d9b3a..975903159be 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -128,7 +128,7 @@ window.DropzoneInput = (function() {
// removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue.
$cancelButton.on('click', (e) => {
- const target = e.target.closest('form').querySelector('.div-dropzone');
+ const target = e.target.closest('.js-main-target-form').querySelector('.div-dropzone');
e.preventDefault();
e.stopPropagation();
@@ -140,7 +140,7 @@ window.DropzoneInput = (function() {
// and add that files to the dropzone files queue again.
// addFile() adds file to dropzone files queue and upload it.
$retryLink.on('click', (e) => {
- const dropzoneInstance = Dropzone.forElement(e.target.closest('form').querySelector('.div-dropzone'));
+ const dropzoneInstance = Dropzone.forElement(e.target.closest('.js-main-target-form').querySelector('.div-dropzone'));
const failedFiles = dropzoneInstance.files;
e.preventDefault();
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 2856c8e2862..ee71728184f 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -1,7 +1,7 @@
/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */
/* global dateFormat */
-/* global Pikaday */
+import Pikaday from 'pikaday';
import DateFix from './lib/utils/datefix';
class DueDateSelect {
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
new file mode 100644
index 00000000000..800ca05cd11
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/feature_highlight.js
@@ -0,0 +1,61 @@
+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
new file mode 100644
index 00000000000..9f741355cd7
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
@@ -0,0 +1,57 @@
+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
new file mode 100644
index 00000000000..fd48f2e87cc
--- /dev/null
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_options.js
@@ -0,0 +1,12 @@
+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/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
new file mode 100644
index 00000000000..f9bbbf0cbc1
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -0,0 +1,82 @@
+/* global Flash */
+
+import Ajax from '~/droplab/plugins/ajax';
+import Filter from '~/droplab/plugins/filter';
+import './filtered_search_dropdown';
+
+class DropdownEmoji extends gl.FilteredSearchDropdown {
+ constructor(options = {}) {
+ super(options);
+ this.config = {
+ Ajax: {
+ endpoint: `${gon.relative_url_root || ''}/autocomplete/award_emojis`,
+ method: 'setData',
+ loadingTemplate: this.loadingTemplate,
+ onError() {
+ /* eslint-disable no-new */
+ new Flash('An error occured fetching the dropdown data.');
+ /* eslint-enable no-new */
+ },
+ },
+ Filter: {
+ template: 'name',
+ },
+ };
+
+ import(/* webpackChunkName: 'emoji' */ '~/emoji')
+ .then(({ glEmojiTag }) => { this.glEmojiTag = glEmojiTag; })
+ .catch(() => { /* ignore error and leave emoji name in the search bar */ });
+
+ this.unbindEvents();
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ super.bindEvents();
+
+ this.listRenderedWrapper = this.listRendered.bind(this);
+ this.dropdown.addEventListener('render.dl', this.listRenderedWrapper);
+ }
+
+ unbindEvents() {
+ this.dropdown.removeEventListener('render.dl', this.listRenderedWrapper);
+ super.unbindEvents();
+ }
+
+ listRendered() {
+ this.replaceEmojiElement();
+ }
+
+ itemClicked(e) {
+ super.itemClicked(e, (selected) => {
+ const name = selected.querySelector('.js-data-value').innerText.trim();
+ return gl.DropdownUtils.getEscapedText(name);
+ });
+ }
+
+ renderContent(forceShowList = false) {
+ this.droplab.changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
+ super.renderContent(forceShowList);
+ }
+
+ replaceEmojiElement() {
+ if (!this.glEmojiTag) return;
+
+ // Replace empty gl-emoji tag to real content
+ const dropdownItems = [...this.dropdown.querySelectorAll('.filter-dropdown-item')];
+ dropdownItems.forEach((dropdownItem) => {
+ const name = dropdownItem.querySelector('.js-data-value').innerText;
+ const emojiTag = this.glEmojiTag(name);
+ const emojiElement = dropdownItem.querySelector('gl-emoji');
+ emojiElement.outerHTML = emojiTag;
+ });
+ }
+
+ init() {
+ this.droplab
+ .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
+ }
+}
+
+window.gl = window.gl || {};
+gl.DropdownEmoji = DropdownEmoji;
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index a81389ab088..23040cd9eb8 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -61,7 +61,7 @@ class DropdownHint extends gl.FilteredSearchDropdown {
.map(tokenKey => ({
icon: `fa-${tokenKey.icon}`,
hint: tokenKey.key,
- tag: `<${tokenKey.symbol}${tokenKey.key}>`,
+ tag: `:${tokenKey.tag}`,
type: tokenKey.type,
}));
diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
index 132b6fe698a..6d5dd747224 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
@@ -1,3 +1,4 @@
+import './dropdown_emoji';
import './dropdown_hint';
import './dropdown_non_user';
import './dropdown_user';
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index dd1c067df87..46c80dfd45e 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -58,6 +58,11 @@ class FilteredSearchDropdownManager {
},
element: this.container.querySelector('#js-dropdown-label'),
},
+ 'my-reaction': {
+ reference: null,
+ gl: 'DropdownEmoji',
+ element: this.container.querySelector('#js-dropdown-my-reaction'),
+ },
hint: {
reference: null,
gl: 'DropdownHint',
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index a31be2b0bc7..038239bf466 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -439,8 +439,13 @@ class FilteredSearchManager {
const match = this.filteredSearchTokenKeys.searchByKeyParam(keyParam);
if (match) {
- const indexOf = keyParam.indexOf('_');
- const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
+ // Use lastIndexOf because the token key is allowed to contain underscore
+ // e.g. 'my_reaction' is the token key of 'my_reaction_emoji'
+ const lastIndexOf = keyParam.lastIndexOf('_');
+ let sanitizedKey = lastIndexOf !== -1 ? keyParam.slice(0, lastIndexOf) : keyParam;
+ // Replace underscore with hyphen in the sanitizedkey.
+ // e.g. 'my_reaction' => 'my-reaction'
+ sanitizedKey = sanitizedKey.replace('_', '-');
const symbol = match.symbol;
let quotationsToUse = '';
@@ -515,7 +520,10 @@ class FilteredSearchManager {
const condition = this.filteredSearchTokenKeys
.searchByConditionKeyValue(token.key, token.value.toLowerCase());
const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {};
- const keyParam = param ? `${token.key}_${param}` : token.key;
+ // Replace hyphen with underscore to use as request parameter
+ // e.g. 'my-reaction' => 'my_reaction'
+ const underscoredKey = token.key.replace('-', '_');
+ const keyParam = param ? `${underscoredKey}_${param}` : underscoredKey;
let tokenPath = '';
if (condition) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index 025d4d8795b..be595d7df1a 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -4,26 +4,42 @@ const tokenKeys = [{
param: 'username',
symbol: '@',
icon: 'pencil',
+ tag: '@author',
}, {
key: 'assignee',
type: 'string',
param: 'username',
symbol: '@',
icon: 'user',
+ tag: '@assignee',
}, {
key: 'milestone',
type: 'string',
param: 'title',
symbol: '%',
icon: 'clock-o',
+ tag: '%milestone',
}, {
key: 'label',
type: 'array',
param: 'name[]',
symbol: '~',
icon: 'tag',
+ tag: '~label',
}];
+if (gon.current_user_id) {
+ // Appending tokenkeys only logged-in
+ tokenKeys.push({
+ key: 'my-reaction',
+ type: 'string',
+ param: 'emoji',
+ symbol: '',
+ icon: 'thumbs-up',
+ tag: 'emoji',
+ });
+}
+
const alternativeTokenKeys = [{
key: 'label',
type: 'string',
@@ -84,6 +100,10 @@ class FilteredSearchTokenKeys {
return tokenKeysWithAlternative.find((tokenKey) => {
let tokenKeyParam = tokenKey.key;
+ // Replace hyphen with underscore to compare keyParam with tokenKeyParam
+ // e.g. 'my-reaction' => 'my_reaction'
+ tokenKeyParam = tokenKeyParam.replace('-', '_');
+
if (tokenKey.param) {
tokenKeyParam += `_${tokenKey.param}`;
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index 243ee4d723a..28e8240169d 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -132,6 +132,23 @@ class FilteredSearchVisualTokens {
.catch(() => { });
}
+ static updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
+ const container = tokenValueContainer;
+ const element = tokenValueElement;
+
+ return import(/* webpackChunkName: 'emoji' */ '../emoji')
+ .then((Emoji) => {
+ if (!Emoji.isEmojiNameValid(tokenValue)) {
+ return;
+ }
+
+ container.dataset.originalValue = tokenValue;
+ element.innerHTML = Emoji.glEmojiTag(tokenValue);
+ })
+ // ignore error and leave emoji name in the search bar
+ .catch(() => { });
+ }
+
static renderVisualTokenValue(parentElement, tokenName, tokenValue) {
const tokenValueContainer = parentElement.querySelector('.value-container');
const tokenValueElement = tokenValueContainer.querySelector('.value');
@@ -144,6 +161,10 @@ class FilteredSearchVisualTokens {
FilteredSearchVisualTokens.updateUserTokenAppearance(
tokenValueContainer, tokenValueElement, tokenValue,
);
+ } else if (tokenType === 'my-reaction') {
+ FilteredSearchVisualTokens.updateEmojiTokenAppearance(
+ tokenValueContainer, tokenValueElement, tokenValue,
+ );
}
}
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 301e82f4610..4b19f7b4188 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -1,7 +1,56 @@
-/* global bp */
-import './breakpoints';
+import bp from './breakpoints';
-export const canShowSubItems = () => bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
+const HIDE_INTERVAL_TIMEOUT = 300;
+const IS_OVER_CLASS = 'is-over';
+const IS_ABOVE_CLASS = 'is-above';
+const IS_SHOWING_FLY_OUT_CLASS = 'is-showing-fly-out';
+let currentOpenMenu = null;
+let menuCornerLocs;
+let timeoutId;
+let sidebar;
+
+export const mousePos = [];
+
+export const setSidebar = (el) => { sidebar = el; };
+export const getOpenMenu = () => currentOpenMenu;
+export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; };
+
+export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
+
+let headerHeight = 50;
+
+export const getHeaderHeight = () => headerHeight;
+
+export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-icons-only');
+
+export const canShowActiveSubItems = (el) => {
+ if (el.classList.contains('active') && !isSidebarCollapsed()) {
+ return false;
+ }
+
+ return true;
+};
+
+export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
+
+export const getHideSubItemsInterval = () => {
+ if (!currentOpenMenu) return 0;
+
+ const currentMousePos = mousePos[mousePos.length - 1];
+ const prevMousePos = mousePos[0];
+ const currentMousePosY = currentMousePos.y;
+ const [menuTop, menuBottom] = menuCornerLocs;
+
+ if (currentMousePosY < menuTop.y ||
+ currentMousePosY > menuBottom.y) return 0;
+
+ if (slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) &&
+ slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)) {
+ return HIDE_INTERVAL_TIMEOUT;
+ }
+
+ return 0;
+};
export const calculateTop = (boundingRect, outerHeight) => {
const windowHeight = window.innerHeight;
@@ -11,42 +60,126 @@ export const calculateTop = (boundingRect, outerHeight) => {
boundingRect.top;
};
-export const showSubLevelItems = (el) => {
- const subItems = el.querySelector('.sidebar-sub-level-items');
+export const hideMenu = (el) => {
+ if (!el) return;
- if (!subItems || !canShowSubItems()) return;
+ const parentEl = el.parentNode;
- subItems.style.display = 'block';
- el.classList.add('is-over');
+ el.style.display = ''; // eslint-disable-line no-param-reassign
+ el.style.transform = ''; // eslint-disable-line no-param-reassign
+ el.classList.remove(IS_ABOVE_CLASS);
+ parentEl.classList.remove(IS_OVER_CLASS);
+ parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
+ setOpenMenu();
+};
+
+export const moveSubItemsToPosition = (el, subItems) => {
const boundingRect = el.getBoundingClientRect();
const top = calculateTop(boundingRect, subItems.offsetHeight);
const isAbove = top < boundingRect.top;
subItems.classList.add('fly-out-list');
- subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`;
+ subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign
+
+ const subItemsRect = subItems.getBoundingClientRect();
+
+ menuCornerLocs = [
+ {
+ x: subItemsRect.left, // left position of the sub items
+ y: subItemsRect.top, // top position of the sub items
+ },
+ {
+ x: subItemsRect.left, // left position of the sub items
+ y: subItemsRect.top + subItemsRect.height, // bottom position of the sub items
+ },
+ ];
if (isAbove) {
- subItems.classList.add('is-above');
+ subItems.classList.add(IS_ABOVE_CLASS);
}
};
-export const hideSubLevelItems = (el) => {
+export const showSubLevelItems = (el) => {
+ const subItems = el.querySelector('.sidebar-sub-level-items');
+ const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only');
+
+ if (!canShowSubItems() || !canShowActiveSubItems(el)) return;
+
+ el.classList.add(IS_OVER_CLASS);
+
+ if (!subItems || (!isSidebarCollapsed() && isIconOnly)) return;
+
+ subItems.style.display = 'block';
+ el.classList.add(IS_SHOWING_FLY_OUT_CLASS);
+
+ setOpenMenu(subItems);
+ moveSubItemsToPosition(el, subItems);
+};
+
+export const mouseEnterTopItems = (el) => {
+ clearTimeout(timeoutId);
+
+ timeoutId = setTimeout(() => {
+ if (currentOpenMenu) hideMenu(currentOpenMenu);
+
+ showSubLevelItems(el);
+ }, getHideSubItemsInterval());
+};
+
+export const mouseLeaveTopItem = (el) => {
const subItems = el.querySelector('.sidebar-sub-level-items');
- if (!subItems || !canShowSubItems()) return;
+ if (!canShowSubItems() || !canShowActiveSubItems(el) ||
+ (subItems && subItems === currentOpenMenu)) return;
+
+ el.classList.remove(IS_OVER_CLASS);
+};
+
+export const documentMouseMove = (e) => {
+ mousePos.push({
+ x: e.clientX,
+ y: e.clientY,
+ });
- el.classList.remove('is-over');
- subItems.style.display = 'none';
- subItems.classList.remove('is-above');
+ if (mousePos.length > 6) mousePos.shift();
+};
+
+export const subItemsMouseLeave = (relatedTarget) => {
+ clearTimeout(timeoutId);
+
+ if (!relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
+ hideMenu(currentOpenMenu);
+ }
};
export default () => {
- const items = [...document.querySelectorAll('.sidebar-top-level-items > li:not(.active)')]
- .filter(el => el.querySelector('.sidebar-sub-level-items'));
+ sidebar = document.querySelector('.nav-sidebar');
+
+ if (!sidebar) return;
+
+ const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
+
+ sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
+ clearTimeout(timeoutId);
+
+ timeoutId = setTimeout(() => {
+ if (currentOpenMenu) hideMenu(currentOpenMenu);
+ }, getHideSubItemsInterval());
+ });
+
+ headerHeight = document.querySelector('.nav-sidebar').offsetTop;
items.forEach((el) => {
- el.addEventListener('mouseenter', e => showSubLevelItems(e.currentTarget));
- el.addEventListener('mouseleave', e => hideSubLevelItems(e.currentTarget));
+ const subItems = el.querySelector('.sidebar-sub-level-items');
+
+ if (subItems) {
+ subItems.addEventListener('mouseleave', e => subItemsMouseLeave(e.relatedTarget));
+ }
+
+ el.addEventListener('mouseenter', e => mouseEnterTopItems(e.currentTarget));
+ el.addEventListener('mouseleave', e => mouseLeaveTopItem(e.currentTarget));
});
+
+ document.addEventListener('mousemove', documentMouseMove);
};
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index b62acfcd445..50d822eba5a 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -175,7 +175,7 @@ GitLabDropdownFilter = (function() {
elements.show().removeClass('option-hidden');
}
- elements.parent().find('.dropdown-menu-empty-link').toggleClass('hidden', elements.is(':visible'));
+ elements.parent().find('.dropdown-menu-empty-item').toggleClass('hidden', elements.is(':visible'));
}
};
@@ -247,7 +247,7 @@ GitLabDropdown = (function() {
currentIndex = -1;
- NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+ NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
@@ -486,7 +486,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.shouldPropagate = function(e) {
var $target;
- if (this.options.multiSelect) {
+ if (this.options.multiSelect || this.options.shouldPropagate === false) {
$target = $(e.target);
if ($target && !$target.hasClass('dropdown-menu-close') &&
!$target.hasClass('dropdown-menu-close-icon') &&
@@ -546,10 +546,10 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.positionMenuAbove = function() {
- var $button = $(this.el);
var $menu = this.dropdown.find('.dropdown-menu');
- $menu.css('top', ($button.height() + $menu.height()) * -1);
+ $menu.css('top', 'initial');
+ $menu.css('bottom', '100%');
};
GitLabDropdown.prototype.hidden = function(e) {
@@ -637,11 +637,15 @@ GitLabDropdown = (function() {
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName;
- if (value) { value = value.toString().replace(/'/g, '\\\''); }
-
- field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
- if (field.length) {
- selected = true;
+ if (value) {
+ value = value.toString().replace(/'/g, '\\\'');
+ field = this.dropdown.parent().find(`input[name='${fieldName}'][value='${value}']`);
+ if (field.length) {
+ selected = true;
+ }
+ } else {
+ field = this.dropdown.parent().find(`input[name='${fieldName}']`);
+ selected = !field.length;
}
}
// Set URL
@@ -698,7 +702,7 @@ GitLabDropdown = (function() {
GitLabDropdown.prototype.noResults = function() {
var html;
- return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
+ return '<li class="dropdown-menu-empty-item"><a>No matching results</a></li>';
};
GitLabDropdown.prototype.rowClicked = function(el) {
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index 1c379e9bb67..7ac9dcd1112 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -1,12 +1,14 @@
export default class GpgBadges {
static fetch() {
+ const badges = $('.js-loading-gpg-badge');
const form = $('.commits-search-form');
+ badges.html('<i class="fa fa-spinner fa-spin"></i>');
+
$.get({
url: form.data('signatures-path'),
data: form.serialize(),
}).done((response) => {
- const badges = $('.js-loading-gpg-badge');
response.signatures.forEach((signature) => {
badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html);
});
diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js
deleted file mode 100644
index 3e483b69fd2..00000000000
--- a/app/assets/javascripts/group_name.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import Cookies from 'js-cookie';
-import _ from 'underscore';
-
-export default class GroupName {
- constructor() {
- this.titleContainer = document.querySelector('.js-title-container');
- this.title = this.titleContainer.querySelector('.title');
-
- if (this.title) {
- this.titleWidth = this.title.offsetWidth;
- this.groupTitle = this.titleContainer.querySelector('.group-title');
- this.groups = this.titleContainer.querySelectorAll('.group-path');
- this.toggle = null;
- this.isHidden = false;
- this.init();
- }
- }
-
- init() {
- if (this.groups.length > 0) {
- this.groups[this.groups.length - 1].classList.remove('hidable');
- this.toggleHandler();
- window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100));
- }
- this.render();
- }
-
- toggleHandler() {
- if (this.titleWidth > this.titleContainer.offsetWidth) {
- if (!this.toggle) this.createToggle();
- this.showToggle();
- } else if (this.toggle) {
- this.hideToggle();
- }
- }
-
- createToggle() {
- this.toggle = document.createElement('button');
- this.toggle.setAttribute('type', 'button');
- this.toggle.className = 'text-expander group-name-toggle';
- this.toggle.setAttribute('aria-label', 'Toggle full path');
- if (Cookies.get('new_nav') === 'true') {
- this.toggle.innerHTML = '<i class="fa fa-ellipsis-h" aria-hidden="true"></i>';
- } else {
- this.toggle.innerHTML = '...';
- }
- this.toggle.addEventListener('click', this.toggleGroups.bind(this));
- if (Cookies.get('new_nav') === 'true') {
- this.title.insertBefore(this.toggle, this.groupTitle);
- } else {
- this.titleContainer.insertBefore(this.toggle, this.title);
- }
- this.toggleGroups();
- }
-
- showToggle() {
- this.title.classList.add('wrap');
- this.toggle.classList.remove('hidden');
- if (this.isHidden) this.groupTitle.classList.add('hidden');
- }
-
- hideToggle() {
- this.title.classList.remove('wrap');
- this.toggle.classList.add('hidden');
- if (this.isHidden) this.groupTitle.classList.remove('hidden');
- }
-
- toggleGroups() {
- this.isHidden = !this.isHidden;
- this.groupTitle.classList.toggle('hidden');
- }
-
- render() {
- this.title.classList.remove('initializing');
- }
-}
diff --git a/app/assets/javascripts/groups/components/group_identicon.vue b/app/assets/javascripts/groups/components/group_identicon.vue
deleted file mode 100644
index 0edd820743f..00000000000
--- a/app/assets/javascripts/groups/components/group_identicon.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<script>
-export default {
- props: {
- entityId: {
- type: Number,
- required: true,
- },
- entityName: {
- type: String,
- required: true,
- },
- },
- computed: {
- /**
- * This method is based on app/helpers/application_helper.rb#project_identicon
- */
- identiconStyles() {
- const allowedColors = [
- '#FFEBEE',
- '#F3E5F5',
- '#E8EAF6',
- '#E3F2FD',
- '#E0F2F1',
- '#FBE9E7',
- '#EEEEEE',
- ];
-
- const backgroundColor = allowedColors[this.entityId % 7];
-
- return `background-color: ${backgroundColor}; color: #555;`;
- },
- identiconTitle() {
- return this.entityName.charAt(0).toUpperCase();
- },
- },
-};
-</script>
-
-<template>
- <div
- class="avatar s40 identicon"
- :style="identiconStyles">
- {{identiconTitle}}
- </div>
-</template>
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index cb133cf7535..2060410e991 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -1,10 +1,10 @@
<script>
+import identicon from '../../vue_shared/components/identicon.vue';
import eventHub from '../event_hub';
-import groupIdenticon from './group_identicon.vue';
export default {
components: {
- groupIdenticon,
+ identicon,
},
props: {
group: {
@@ -205,7 +205,7 @@ export default {
class="avatar s40"
:src="group.avatarUrl"
/>
- <group-identicon
+ <identicon
v-else
:entity-id=group.id
:entity-name="group.name"
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index b5975295329..4d629bc6326 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -111,8 +111,7 @@ window.GroupsSelect = (function() {
};
GroupsSelect.prototype.forceOverflow = function (e) {
- const itemHeight = this.dropdown.querySelector('.select2-result:first-child').clientHeight;
- this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight - (itemHeight * 0.9))}px`;
+ this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`;
};
GroupsSelect.PER_PAGE = 20;
diff --git a/app/assets/javascripts/helpers/user_feature_helper.js b/app/assets/javascripts/helpers/user_feature_helper.js
index fcd8569819c..638118a5204 100644
--- a/app/assets/javascripts/helpers/user_feature_helper.js
+++ b/app/assets/javascripts/helpers/user_feature_helper.js
@@ -1,11 +1,7 @@
import Cookies from 'js-cookie';
-function isNewRepo() {
- return Cookies.get('new_repo') === 'true';
-}
-
-const UserFeatureHelper = {
- isNewRepo,
+export default {
+ isNewRepoEnabled() {
+ return Cookies.get('new_repo') === 'true';
+ },
};
-
-export default UserFeatureHelper;
diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js
new file mode 100644
index 00000000000..f785ed29e6c
--- /dev/null
+++ b/app/assets/javascripts/init_changes_dropdown.js
@@ -0,0 +1,10 @@
+import stickyMonitor from './lib/utils/sticky';
+
+export default () => {
+ stickyMonitor(document.querySelector('.js-diff-files-changed'));
+
+ $('.js-diff-stats-dropdown').glDropdown({
+ filterable: true,
+ remoteFilter: false,
+ });
+};
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index d314f3c4d43..0e8a0519928 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -5,7 +5,6 @@
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
-import SidebarHeightManager from './sidebar_height_manager';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
@@ -50,13 +49,6 @@ export default class IssuableBulkUpdateSidebar {
new SubscriptionSelect();
}
- getNavHeight() {
- const navbarHeight = $('.navbar-gitlab').outerHeight();
- const layoutNavHeight = $('.layout-nav').outerHeight();
- const subNavScroll = $('.sub-nav-scroll').outerHeight();
- return navbarHeight + layoutNavHeight + subNavScroll;
- }
-
setupBulkUpdateActions() {
IssuableBulkUpdateActions.setOriginalDropdownData();
}
@@ -84,23 +76,6 @@ export default class IssuableBulkUpdateSidebar {
this.toggleBulkEditButtonDisabled(enable);
this.toggleOtherFiltersDisabled(enable);
this.toggleCheckboxDisplay(enable);
-
- if (enable) {
- this.initAffix();
- SidebarHeightManager.init();
- }
- }
-
- initAffix() {
- if (!this.$sidebar.hasClass('affix-top')) {
- const offsetTop = $('.scrolling-tabs-container').outerHeight() + $('.sub-nav-scroll').outerHeight();
-
- this.$sidebar.affix({
- offset: {
- top: offsetTop,
- },
- });
- }
}
updateSelectedIssuableIds() {
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 26392db4b5b..70c364e51fe 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,7 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */
-/* global bp */
-
import Cookies from 'js-cookie';
+import bp from './breakpoints';
import UsersSelect from './users_select';
const PARTICIPANTS_ROW_COUNT = 7;
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 9ac1325fc95..470c39c6f76 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -2,16 +2,14 @@
/* global GitLab */
/* global Autosave */
/* global dateFormat */
-/* global Pikaday */
+import Pikaday from 'pikaday';
import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
(function() {
this.IssuableForm = (function() {
- IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?';
-
IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
function IssuableForm(form) {
@@ -26,7 +24,6 @@ import ZenMode from './zen_mode';
new ZenMode();
this.titleField = this.form.find("input[name*='[title]']");
this.descriptionField = this.form.find("textarea[name*='[description]']");
- this.issueMoveField = this.form.find("#move_to_project_id");
if (!(this.titleField.length && this.descriptionField.length)) {
return;
}
@@ -34,7 +31,6 @@ import ZenMode from './zen_mode';
this.form.on("submit", this.handleSubmit);
this.form.on("click", ".btn-cancel", this.resetAutosave);
this.initWip();
- this.initMoveDropdown();
$issuableDueDate = $('#issuable-due-date');
if ($issuableDueDate.length) {
calendar = new Pikaday({
@@ -56,12 +52,6 @@ import ZenMode from './zen_mode';
};
IssuableForm.prototype.handleSubmit = function() {
- var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null;
- if ((parseInt(fieldId, 10) || 0) > 0) {
- if (!confirm(this.issueMoveConfirmMsg)) {
- return false;
- }
- }
return this.resetAutosave();
};
@@ -113,48 +103,6 @@ import ZenMode from './zen_mode';
return this.titleField.val("WIP: " + (this.titleField.val()));
};
- IssuableForm.prototype.initMoveDropdown = function() {
- var $moveDropdown, pageSize;
- $moveDropdown = $('.js-move-dropdown');
- if ($moveDropdown.length) {
- pageSize = $moveDropdown.data('page-size');
- return $('.js-move-dropdown').select2({
- ajax: {
- url: $moveDropdown.data('projects-url'),
- quietMillis: 125,
- data: function(term, page, context) {
- return {
- search: term,
- offset_id: context
- };
- },
- results: function(data) {
- var context,
- more;
-
- if (data.length >= pageSize)
- more = true;
-
- if (data[data.length - 1])
- context = data[data.length - 1].id;
-
- return {
- results: data,
- more: more,
- context: context
- };
- }
- },
- formatResult: function(project) {
- return project.name_with_namespace;
- },
- formatSelection: function(project) {
- return project.name_with_namespace;
- }
- });
- }
- };
-
return IssuableForm;
})();
}).call(window);
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 2bee4fb045a..c0bd64814ca 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -42,7 +42,7 @@ class Issue {
initIssueBtnEventListeners() {
const issueFailMessage = 'Unable to update this issue at this time.';
- return $(document).on('click', 'a.btn-close, a.btn-reopen', (e) => {
+ return $(document).on('click', '.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen', (e) => {
var $button, shouldSubmit, url;
e.preventDefault();
e.stopImmediatePropagation();
@@ -66,15 +66,14 @@ class Issue {
const projectIssuesCounter = $('.issue_counter');
if ('id' in data) {
- $(document).trigger('issuable:change');
-
const isClosed = $button.hasClass('btn-close');
isClosedBadge.toggleClass('hidden', !isClosed);
isOpenBadge.toggleClass('hidden', isClosed);
+ $(document).trigger('issuable:change', isClosed);
this.toggleCloseReopenButton(isClosed);
- let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
+ let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
@@ -121,7 +120,7 @@ class Issue {
static submitNoteForm(form) {
var noteText;
noteText = form.find("textarea.js-note-text").val();
- if (noteText.trim().length > 0) {
+ if (noteText && noteText.trim().length > 0) {
return form.submit();
}
}
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index efae112923d..e115ee40219 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -17,10 +17,6 @@ export default {
required: true,
type: String,
},
- canMove: {
- required: true,
- type: Boolean,
- },
canUpdate: {
required: true,
type: Boolean,
@@ -80,11 +76,11 @@ export default {
type: Boolean,
required: true,
},
- markdownPreviewUrl: {
+ markdownPreviewPath: {
type: String,
required: true,
},
- markdownDocs: {
+ markdownDocsPath: {
type: String,
required: true,
},
@@ -96,10 +92,6 @@ export default {
type: String,
required: true,
},
- projectsAutocompleteUrl: {
- type: String,
- required: true,
- },
},
data() {
const store = new Store({
@@ -142,7 +134,6 @@ export default {
confidential: this.isConfidential,
description: this.state.descriptionText,
lockedWarningVisible: false,
- move_to_project_id: 0,
updateLoading: false,
});
}
@@ -151,16 +142,6 @@ export default {
this.showForm = false;
},
updateIssuable() {
- const canPostUpdate = this.store.formState.move_to_project_id !== 0 ?
- confirm('Are you sure you want to move this issue to another project?') : true; // eslint-disable-line no-alert
-
- if (!canPostUpdate) {
- this.store.setFormState({
- updateLoading: false,
- });
- return;
- }
-
this.service.updateIssuable(this.store.formState)
.then(res => res.json())
.then((data) => {
@@ -239,14 +220,12 @@ export default {
<form-component
v-if="canUpdate && showForm"
:form-state="formState"
- :can-move="canMove"
:can-destroy="canDestroy"
:issuable-templates="issuableTemplates"
- :markdown-docs="markdownDocs"
- :markdown-preview-url="markdownPreviewUrl"
+ :markdown-docs-path="markdownDocsPath"
+ :markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
- :projects-autocomplete-url="projectsAutocompleteUrl"
/>
<div v-else>
<title-component
diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue
index d59e6d11032..992b7064c13 100644
--- a/app/assets/javascripts/issue_show/components/edited.vue
+++ b/app/assets/javascripts/issue_show/components/edited.vue
@@ -37,7 +37,7 @@ export default {
Edited
<time-ago-tooltip
v-if="updatedAt"
- placement="bottom"
+ tooltip-placement="bottom"
:time="updatedAt"
/>
<span
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 27b1b814f9a..dc902eefc5f 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -10,11 +10,11 @@
type: Object,
required: true,
},
- markdownPreviewUrl: {
+ markdownPreviewPath: {
type: String,
required: true,
},
- markdownDocs: {
+ markdownDocsPath: {
type: String,
required: true,
},
@@ -36,8 +36,8 @@
Description
</label>
<markdown-field
- :markdown-preview-url="markdownPreviewUrl"
- :markdown-docs="markdownDocs">
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath">
<textarea
id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area"
diff --git a/app/assets/javascripts/issue_show/components/fields/project_move.vue b/app/assets/javascripts/issue_show/components/fields/project_move.vue
deleted file mode 100644
index 7bf2be8b28a..00000000000
--- a/app/assets/javascripts/issue_show/components/fields/project_move.vue
+++ /dev/null
@@ -1,83 +0,0 @@
-<script>
- import tooltip from '../../../vue_shared/directives/tooltip';
-
- export default {
- directives: {
- tooltip,
- },
- props: {
- formState: {
- type: Object,
- required: true,
- },
- projectsAutocompleteUrl: {
- type: String,
- required: true,
- },
- },
- mounted() {
- const $moveDropdown = $(this.$refs['move-dropdown']);
-
- $moveDropdown.select2({
- ajax: {
- url: this.projectsAutocompleteUrl,
- quietMillis: 125,
- data(term, page, context) {
- return {
- search: term,
- offset_id: context,
- };
- },
- results(data) {
- const more = data.length >= 50;
- const context = data[data.length - 1] ? data[data.length - 1].id : null;
-
- return {
- results: data,
- more,
- context,
- };
- },
- },
- formatResult(project) {
- return project.name_with_namespace;
- },
- formatSelection(project) {
- return project.name_with_namespace;
- },
- })
- .on('change', (e) => {
- this.formState.move_to_project_id = parseInt(e.target.value, 10);
- });
- },
- beforeDestroy() {
- $(this.$refs['move-dropdown']).select2('destroy');
- },
- };
-</script>
-
-<template>
- <fieldset>
- <label
- for="issuable-move"
- class="sr-only">
- Move
- </label>
- <div class="issuable-form-select-holder append-right-5">
- <input
- ref="move-dropdown"
- type="hidden"
- id="issuable-move"
- data-placeholder="Move to a different project" />
- </div>
- <span
- v-tooltip
- data-placement="auto top"
- title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.">
- <i
- class="fa fa-question-circle"
- aria-hidden="true">
- </i>
- </span>
- </fieldset>
-</template>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 76ec3dc9a5d..6a2dd502fe2 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -4,15 +4,10 @@
import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue';
- import projectMove from './fields/project_move.vue';
import confidentialCheckbox from './fields/confidential_checkbox.vue';
export default {
props: {
- canMove: {
- type: Boolean,
- required: true,
- },
canDestroy: {
type: Boolean,
required: true,
@@ -26,11 +21,11 @@
required: false,
default: () => [],
},
- markdownPreviewUrl: {
+ markdownPreviewPath: {
type: String,
required: true,
},
- markdownDocs: {
+ markdownDocsPath: {
type: String,
required: true,
},
@@ -42,10 +37,6 @@
type: String,
required: true,
},
- projectsAutocompleteUrl: {
- type: String,
- required: true,
- },
},
components: {
lockedWarning,
@@ -53,7 +44,6 @@
descriptionField,
descriptionTemplate,
editActions,
- projectMove,
confidentialCheckbox,
},
computed: {
@@ -89,14 +79,10 @@
</div>
<description-field
:form-state="formState"
- :markdown-preview-url="markdownPreviewUrl"
- :markdown-docs="markdownDocs" />
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath" />
<confidential-checkbox
:form-state="formState" />
- <project-move
- v-if="canMove"
- :form-state="formState"
- :projects-autocomplete-url="projectsAutocompleteUrl" />
<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 ad8cb6465e2..8053ef57e6c 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -28,7 +28,6 @@ document.addEventListener('DOMContentLoaded', () => {
props: {
canUpdate: this.canUpdate,
canDestroy: this.canDestroy,
- canMove: this.canMove,
endpoint: this.endpoint,
issuableRef: this.issuableRef,
initialTitleHtml: this.initialTitleHtml,
@@ -37,11 +36,10 @@ document.addEventListener('DOMContentLoaded', () => {
initialDescriptionText: this.initialDescriptionText,
issuableTemplates: this.issuableTemplates,
isConfidential: this.isConfidential,
- markdownPreviewUrl: this.markdownPreviewUrl,
- markdownDocs: this.markdownDocs,
+ markdownPreviewPath: this.markdownPreviewPath,
+ markdownDocsPath: this.markdownDocsPath,
projectPath: this.projectPath,
projectNamespace: this.projectNamespace,
- projectsAutocompleteUrl: this.projectsAutocompleteUrl,
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js
index 0c8bd6f1cc3..f4639e9ed2a 100644
--- a/app/assets/javascripts/issue_show/stores/index.js
+++ b/app/assets/javascripts/issue_show/stores/index.js
@@ -6,7 +6,6 @@ export default class Store {
confidential: false,
description: '',
lockedWarningVisible: false,
- move_to_project_id: 0,
updateLoading: false,
};
}
diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue
index 5b9cf577189..3f6f40d47ba 100644
--- a/app/assets/javascripts/jobs/components/header.vue
+++ b/app/assets/javascripts/jobs/components/header.vue
@@ -40,7 +40,7 @@
label: 'New issue',
path: this.job.new_issue_path,
cssClass: 'js-new-issue btn btn-new btn-inverted visible-md-block visible-lg-block',
- type: 'ujs-link',
+ type: 'link',
});
}
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 5c1ba416a03..d064a2c0024 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -50,19 +50,10 @@ import initFlyOutNav from './fly_out_nav';
});
});
- function applyScrollNavClass() {
- const scrollOpacityHeight = 40;
- $('.navbar-border').css('opacity', Math.min($(window).scrollTop() / scrollOpacityHeight, 1));
- }
-
$(() => {
- if (Cookies.get('new_nav') === 'true') {
- const newNavSidebar = new NewNavSidebar();
- newNavSidebar.bindEvents();
-
- initFlyOutNav();
- }
+ const newNavSidebar = new NewNavSidebar();
+ newNavSidebar.bindEvents();
- $(window).on('scroll', _.throttle(applyScrollNavClass, 100));
+ initFlyOutNav();
});
}).call(window);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index e916724b666..b8bebe1894f 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -27,6 +27,13 @@
}
};
+ 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",
@@ -167,11 +174,12 @@
};
gl.utils.scrollToElement = function($el) {
- var top = $el.offset().top;
- gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();
+ const top = $el.offset().top;
+ const mrTabsHeight = $('.merge-request-tabs').height() || 0;
+ const headerHeight = $('.navbar-gitlab').height() || 0;
return $('body, html').animate({
- scrollTop: top - (gl.mrTabsHeight)
+ scrollTop: top - mrTabsHeight - headerHeight,
}, 200);
};
@@ -378,15 +386,15 @@
w.gl.utils.backOff = (fn, timeout = 60000) => {
const maxInterval = 32000;
let nextInterval = 2000;
-
- const startTime = Date.now();
+ let timeElapsed = 0;
return new Promise((resolve, reject) => {
const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
const next = () => {
- if (Date.now() - startTime < timeout) {
- setTimeout(fn.bind(null, next, stop), nextInterval);
+ if (timeElapsed < timeout) {
+ setTimeout(() => fn(next, stop), nextInterval);
+ timeElapsed += nextInterval;
nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
} else {
reject(new Error('BACKOFF_TIMEOUT'));
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 1e96c7ab5cd..7a72509d234 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -1,2 +1,3 @@
/* eslint-disable import/prefer-default-export */
export const BYTES_IN_KIB = 1024;
+export const HIDDEN_CLASS = 'hidden';
diff --git a/app/assets/javascripts/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js
index 716aefbfcb7..227bf65b560 100644
--- a/app/assets/javascripts/lib/utils/pretty_time.js
+++ b/app/assets/javascripts/lib/utils/pretty_time.js
@@ -2,19 +2,20 @@ import _ from 'underscore';
(() => {
/*
- * TODO: Make these methods more configurable (e.g. parseSeconds timePeriodContstraints,
- * stringifyTime condensed or non-condensed, abbreviateTimelengths)
+ * 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.
+ * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
+ * or week length.
*/
- parseSeconds(seconds) {
- const DAYS_PER_WEEK = 5;
- const HOURS_PER_DAY = 8;
+ 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;
diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js
index 43a808b6ab3..283c0ec0410 100644
--- a/app/assets/javascripts/lib/utils/sticky.js
+++ b/app/assets/javascripts/lib/utils/sticky.js
@@ -1,7 +1,7 @@
export const isSticky = (el, scrollY, stickyTop) => {
- const top = el.offsetTop - scrollY;
+ const top = Math.floor(el.offsetTop - scrollY);
- if (top === stickyTop) {
+ if (top <= stickyTop) {
el.classList.add('is-stuck');
} else {
el.classList.remove('is-stuck');
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 42092a34c2f..0bc31a56684 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -1,5 +1,4 @@
/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */
-/* global bp */
/* global Flash */
/* global ConfirmDangerModal */
/* global Aside */
@@ -7,7 +6,6 @@
import jQuery from 'jquery';
import _ from 'underscore';
import Cookies from 'js-cookie';
-import Pikaday from 'pikaday';
import Dropzone from 'dropzone';
import Sortable from 'vendor/Sortable';
@@ -20,7 +18,6 @@ import 'vendor/fuzzaldrin-plus';
window.jQuery = jQuery;
window.$ = jQuery;
window._ = _;
-window.Pikaday = Pikaday;
window.Dropzone = Dropzone;
window.Sortable = Sortable;
@@ -68,7 +65,7 @@ import './api';
import './aside';
import './autosave';
import loadAwardsHandler from './awards_handler';
-import './breakpoints';
+import bp from './breakpoints';
import './broadcast_message';
import './build';
import './build_artifacts';
@@ -105,6 +102,7 @@ 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';
@@ -134,9 +132,11 @@ import './project_new';
import './project_select';
import './project_show';
import './project_variables';
+import './projects_dropdown';
import './projects_list';
-import './render_gfm';
+import './syntax_highlight';
import './render_math';
+import './render_gfm';
import './right_sidebar';
import './search';
import './search_autocomplete';
@@ -144,7 +144,7 @@ import './smart_interval';
import './star';
import './subscription';
import './subscription_select';
-import './syntax_highlight';
+import initBreadcrumbs from './breadcrumb';
import './dispatcher';
@@ -182,6 +182,8 @@ $(function () {
var bootstrapBreakpoint = bp.getBreakpointSize();
var fitSidebarForSize;
+ initBreadcrumbs();
+
// Set the default path for all cookies to GitLab's root directory
Cookies.defaults.path = gon.relative_url_root || '/';
@@ -251,7 +253,10 @@ $(function () {
// Initialize popovers
$body.popover({
selector: '[data-toggle="popover"]',
- trigger: 'focus'
+ trigger: 'focus',
+ // set the viewport to the main content, excluding the navigation bar, so
+ // the navigation can't overlap the popover
+ viewport: '.page-with-sidebar'
});
$('.trigger-submit').on('change', function () {
return $(this).parents('form').submit();
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index e034729bd39..cc9016e74da 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -1,5 +1,7 @@
-/* global Pikaday */
/* global dateFormat */
+
+import Pikaday from 'pikaday';
+
(() => {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 4ffd71d9de5..0c3c877ff15 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,13 +1,12 @@
/* eslint-disable no-new, class-methods-use-this */
-/* global Breakpoints */
/* global Flash */
/* global notes */
import Cookies from 'js-cookie';
-import './breakpoints';
import './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
-import stickyMonitor from './lib/utils/sticky';
+import initChangesDropdown from './init_changes_dropdown';
+import bp from './breakpoints';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -134,7 +133,7 @@ import stickyMonitor from './lib/utils/sticky';
this.destroyPipelinesView();
} else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href'));
- if (Breakpoints.get().getBreakpointSize() !== 'lg') {
+ if (bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
}
if (this.diffViewType() === 'parallel') {
@@ -145,7 +144,7 @@ import stickyMonitor from './lib/utils/sticky';
this.resetViewContainer();
this.mountPipelinesView();
} else {
- if (Breakpoints.get().getBreakpointSize() !== 'xs') {
+ if (bp.getBreakpointSize() !== 'xs') {
this.expandView();
}
this.resetViewContainer();
@@ -244,6 +243,7 @@ import stickyMonitor from './lib/utils/sticky';
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
+ autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
},
}).$mount();
@@ -254,6 +254,7 @@ import stickyMonitor from './lib/utils/sticky';
loadDiff(source) {
if (this.diffsLoaded) {
+ document.dispatchEvent(new CustomEvent('scroll'));
return;
}
@@ -267,9 +268,7 @@ import stickyMonitor from './lib/utils/sticky';
const $container = $('#diffs');
$container.html(data.html);
- this.initChangesDropdown();
-
- stickyMonitor(document.querySelector('.js-diff-files-changed'));
+ initChangesDropdown();
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
@@ -319,13 +318,6 @@ import stickyMonitor from './lib/utils/sticky';
});
}
- initChangesDropdown() {
- $('.js-diff-stats-dropdown').glDropdown({
- filterable: true,
- remoteFilter: false,
- });
- }
-
// Show or hide the loading spinner
//
// status - Boolean, true to show, false to hide
@@ -401,7 +393,7 @@ import stickyMonitor from './lib/utils/sticky';
// Screen space on small screens is usually very sparse
// So we dont affix the tabs on these
- if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
+ if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return;
/**
If the browser does not support position sticky, it returns the position as static.
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
new file mode 100644
index 00000000000..b596c4f383f
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -0,0 +1,145 @@
+<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';
+
+ export default {
+
+ data() {
+ const metricsData = document.querySelector('#prometheus-graphs').dataset;
+ const store = new MonitoringStore();
+
+ return {
+ store,
+ state: 'gettingStarted',
+ hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics),
+ documentationPath: metricsData.documentationPath,
+ settingsPath: metricsData.settingsPath,
+ endpoint: metricsData.additionalMetrics,
+ deploymentEndpoint: metricsData.deploymentEndpoint,
+ showEmptyState: true,
+ backOffRequestCounter: 0,
+ updateAspectRatio: false,
+ updatedAspectRatios: 0,
+ resizeThrottled: {},
+ };
+ },
+
+ components: {
+ Graph,
+ GraphGroup,
+ EmptyState,
+ },
+
+ 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.'));
+ },
+
+ resize() {
+ this.updateAspectRatio = true;
+ },
+
+ toggleAspectRatio() {
+ this.updatedAspectRatios = this.updatedAspectRatios += 1;
+ if (this.store.getMetricsCount() === this.updatedAspectRatios) {
+ this.updateAspectRatio = !this.updateAspectRatio;
+ this.updatedAspectRatios = 0;
+ }
+ },
+ },
+
+ created() {
+ this.service = new MonitoringService(this.endpoint);
+ eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
+ },
+
+ beforeDestroy() {
+ eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
+ window.removeEventListener('resize', this.resizeThrottled, false);
+ },
+
+ mounted() {
+ this.resizeThrottled = _.throttle(this.resize, 600);
+ if (!this.hasMetrics) {
+ this.state = 'gettingStarted';
+ } else {
+ this.getGraphsData();
+ window.addEventListener('resize', this.resizeThrottled, false);
+ }
+ },
+ };
+</script>
+
+<template>
+ <div v-if="!showEmptyState" class="prometheus-graphs">
+ <graph-group
+ v-for="(groupData, index) in store.groups"
+ :key="index"
+ :name="groupData.group"
+ >
+ <graph
+ v-for="(graphData, index) in groupData.metrics"
+ :key="index"
+ :graph-data="graphData"
+ :update-aspect-ratio="updateAspectRatio"
+ :deployment-data="store.deploymentData"
+ />
+ </graph-group>
+ </div>
+ <empty-state
+ v-else
+ :selected-state="state"
+ :documentation-path="documentationPath"
+ :settings-path="settingsPath"
+ />
+</template>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
new file mode 100644
index 00000000000..a8708be76de
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -0,0 +1,96 @@
+<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: {
+ type: String,
+ required: true,
+ },
+ settingsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ selectedState: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ states: {
+ gettingStarted: {
+ svg: gettingStartedSvg,
+ 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,
+ 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,
+ title: 'Unable to connect to Prometheus server',
+ description: 'Ensure connectivity is available from the GitLab server to the ',
+ buttonText: 'View documentation',
+ },
+ },
+ };
+ },
+ computed: {
+ currentState() {
+ return this.states[this.selectedState];
+ },
+
+ buttonPath() {
+ if (this.selectedState === 'gettingStarted') {
+ return this.settingsPath;
+ }
+ return this.documentationPath;
+ },
+
+ showButtonDescription() {
+ if (this.selectedState === 'unableToConnect') return true;
+ return false;
+ },
+ },
+ };
+</script>
+
+<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>
+ <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>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
new file mode 100644
index 00000000000..cde2ff7ca2a
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -0,0 +1,275 @@
+<script>
+ import d3 from 'd3';
+ 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 MonitoringMixin from '../mixins/monitoring_mixins';
+ import eventHub from '../event_hub';
+ import measurements from '../utils/measurements';
+ import { timeScaleFormat } 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: {
+ type: Object,
+ required: true,
+ },
+ updateAspectRatio: {
+ type: Boolean,
+ required: true,
+ },
+ deploymentData: {
+ type: Array,
+ required: true,
+ },
+ },
+
+ mixins: [MonitoringMixin],
+
+ data() {
+ return {
+ baseGraphHeight: 450,
+ baseGraphWidth: 600,
+ graphHeight: 450,
+ graphWidth: 600,
+ graphHeightOffset: 120,
+ margin: {},
+ unitOfDisplay: '',
+ areaColorRgb: '#8fbce8',
+ lineColorRgb: '#1f78d1',
+ yAxisLabel: '',
+ legendTitle: '',
+ reducedDeploymentData: [],
+ measurements: measurements.large,
+ currentData: {
+ time: new Date(),
+ value: 0,
+ },
+ currentDataIndex: 0,
+ currentXCoordinate: 0,
+ currentFlagPosition: 0,
+ showFlag: false,
+ showDeployInfo: true,
+ timeSeries: [],
+ };
+ },
+
+ components: {
+ GraphLegend,
+ GraphFlag,
+ GraphDeployment,
+ monitoringPaths,
+ },
+
+ computed: {
+ outterViewBox() {
+ return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
+ },
+
+ innerViewBox() {
+ if ((this.baseGraphWidth - 150) > 0) {
+ return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
+ }
+ return '0 0 0 0';
+ },
+
+ axisTransform() {
+ return `translate(70, ${this.graphHeight - 100})`;
+ },
+
+ paddingBottomRootSvg() {
+ return {
+ paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
+ };
+ },
+ },
+
+ methods: {
+ draw() {
+ const breakpointSize = bp.getBreakpointSize();
+ const query = this.graphData.queries[0];
+ this.margin = measurements.large.margin;
+ if (breakpointSize === 'xs' || breakpointSize === 'sm') {
+ this.graphHeight = 300;
+ this.margin = measurements.small.margin;
+ this.measurements = measurements.small;
+ }
+ this.unitOfDisplay = query.unit || '';
+ this.yAxisLabel = this.graphData.y_label || 'Values';
+ this.legendTitle = query.label || 'Average';
+ this.graphWidth = this.$refs.baseSvg.clientWidth -
+ this.margin.left - this.margin.right;
+ this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
+ this.baseGraphHeight = this.graphHeight;
+ this.baseGraphWidth = this.graphWidth;
+ this.renderAxesPaths();
+ this.formatDeployments();
+ },
+
+ handleMouseOverGraph(e) {
+ let point = this.$refs.graphData.createSVGPoint();
+ point.x = e.clientX;
+ point.y = e.clientY;
+ point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
+ point.x = point.x += 7;
+ const firstTimeSeries = this.timeSeries[0];
+ const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
+ const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
+ const d0 = firstTimeSeries.values[overlayIndex - 1];
+ 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 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;
+ }
+ },
+
+ renderAxesPaths() {
+ this.timeSeries = createTimeSeries(this.graphData.queries[0].result,
+ 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]);
+ const axisYScale = d3.scale.linear()
+ .range([this.graphHeight - this.graphHeightOffset, 0]);
+
+ axisXScale.domain(d3.extent(this.timeSeries[0].values, d => d.time));
+ axisYScale.domain([0, d3.max(this.timeSeries[0].values.map(d => d.value))]);
+
+ const xAxis = d3.svg.axis()
+ .scale(axisXScale)
+ .ticks(measurements.xTicks)
+ .tickFormat(timeScaleFormat)
+ .orient('bottom');
+
+ const yAxis = d3.svg.axis()
+ .scale(axisYScale)
+ .ticks(measurements.yTicks)
+ .orient('left');
+
+ d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
+
+ const width = this.graphWidth;
+ d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis)
+ .selectAll('.tick')
+ .each(function createTickLines(d, i) {
+ if (i > 0) {
+ d3.select(this).select('line')
+ .attr('x2', width)
+ .attr('class', 'axis-tick');
+ } // Avoid adding the class to the first tick, to prevent coloring
+ }); // This will select all of the ticks once they're rendered
+ },
+ },
+
+ watch: {
+ updateAspectRatio() {
+ if (this.updateAspectRatio) {
+ this.graphHeight = 450;
+ this.graphWidth = 600;
+ this.measurements = measurements.large;
+ this.draw();
+ eventHub.$emit('toggleAspectRatio');
+ }
+ },
+ },
+
+ mounted() {
+ this.draw();
+ },
+ };
+</script>
+
+<template>
+ <div class="prometheus-graph">
+ <h5 class="text-center graph-title">
+ {{graphData.title}}
+ </h5>
+ <div
+ class="prometheus-svg-container"
+ :style="paddingBottomRootSvg">
+ <svg
+ :viewBox="outterViewBox"
+ ref="baseSvg">
+ <g
+ class="x-axis"
+ :transform="axisTransform">
+ </g>
+ <g
+ class="y-axis"
+ transform="translate(70, 20)">
+ </g>
+ <graph-legend
+ :graph-width="graphWidth"
+ :graph-height="graphHeight"
+ :margin="margin"
+ :measurements="measurements"
+ :legend-title="legendTitle"
+ :y-axis-label="yAxisLabel"
+ :time-series="timeSeries"
+ :unit-of-display="unitOfDisplay"
+ :current-data-index="currentDataIndex"
+ />
+ <svg
+ class="graph-data"
+ :viewBox="innerViewBox"
+ ref="graphData">
+ <monitoring-paths
+ v-for="(path, index) in timeSeries"
+ :key="index"
+ :generated-line-path="path.linePath"
+ :generated-area-path="path.areaPath"
+ :line-color="path.lineColor"
+ :area-color="path.areaColor"
+ />
+ <monitoring-deployment
+ :show-deploy-info="showDeployInfo"
+ :deployment-data="reducedDeploymentData"
+ :graph-height="graphHeight"
+ :graph-height-offset="graphHeightOffset"
+ />
+ <graph-flag
+ v-if="showFlag"
+ :current-x-coordinate="currentXCoordinate"
+ :current-data="currentData"
+ :current-flag-position="currentFlagPosition"
+ :graph-height="graphHeight"
+ :graph-height-offset="graphHeightOffset"
+ />
+ <rect
+ class="prometheus-graph-overlay"
+ :width="(graphWidth - 70)"
+ :height="(graphHeight - 100)"
+ transform="translate(-5, 20)"
+ ref="graphOverlay"
+ @mousemove="handleMouseOverGraph($event)">
+ </rect>
+ </svg>
+ </svg>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
new file mode 100644
index 00000000000..3623d2ed946
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue
@@ -0,0 +1,133 @@
+<script>
+ import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
+
+ export default {
+ props: {
+ showDeployInfo: {
+ type: Boolean,
+ required: true,
+ },
+ deploymentData: {
+ type: Array,
+ required: true,
+ },
+ graphHeight: {
+ type: Number,
+ required: true,
+ },
+ graphHeightOffset: {
+ type: Number,
+ required: true,
+ },
+ },
+
+ computed: {
+ calculatedHeight() {
+ return this.graphHeight - this.graphHeightOffset;
+ },
+ },
+
+ methods: {
+ refText(d) {
+ return d.tag ? d.ref : d.sha.slice(0, 6);
+ },
+
+ formatTime(deploymentTime) {
+ return timeFormat(deploymentTime);
+ },
+
+ formatDate(deploymentTime) {
+ return dateFormat(deploymentTime);
+ },
+
+ nameDeploymentClass(deployment) {
+ return `deploy-info-${deployment.id}`;
+ },
+
+ transformDeploymentGroup(deployment) {
+ return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
+ },
+ },
+ };
+</script>
+<template>
+ <g
+ class="deploy-info"
+ v-if="showDeployInfo">
+ <g
+ v-for="(deployment, index) in deploymentData"
+ :key="index"
+ :class="nameDeploymentClass(deployment)"
+ :transform="transformDeploymentGroup(deployment)">
+ <rect
+ x="0"
+ y="0"
+ :height="calculatedHeight"
+ width="3"
+ fill="url(#shadow-gradient)">
+ </rect>
+ <line
+ class="deployment-line"
+ x1="0"
+ y1="0"
+ x2="0"
+ :y2="calculatedHeight"
+ stroke="#000">
+ </line>
+ <svg
+ v-if="deployment.showDeploymentFlag"
+ class="js-deploy-info-box"
+ x="3"
+ y="0"
+ width="92"
+ height="60">
+ <rect
+ class="rect-text-metric deploy-info-rect rect-metric"
+ x="1"
+ y="1"
+ rx="2"
+ width="90"
+ height="58">
+ </rect>
+ <g
+ transform="translate(5, 2)">
+ <text
+ class="deploy-info-text text-metric-bold">
+ {{refText(deployment)}}
+ </text>
+ </g>
+ <text
+ class="deploy-info-text"
+ y="18"
+ transform="translate(5, 2)">
+ {{formatDate(deployment.time)}}
+ </text>
+ <text
+ class="deploy-info-text text-metric-bold"
+ y="38"
+ transform="translate(5, 2)">
+ {{formatTime(deployment.time)}}
+ </text>
+ </svg>
+ </g>
+ <svg
+ height="0"
+ width="0">
+ <defs>
+ <linearGradient
+ id="shadow-gradient">
+ <stop
+ offset="0%"
+ stop-color="#000"
+ stop-opacity="0.4">
+ </stop>
+ <stop
+ offset="100%"
+ stop-color="#000"
+ stop-opacity="0">
+ </stop>
+ </linearGradient>
+ </defs>
+ </svg>
+ </g>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
new file mode 100644
index 00000000000..a98e3d06c18
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -0,0 +1,88 @@
+<script>
+ import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
+
+ export default {
+ props: {
+ currentXCoordinate: {
+ type: Number,
+ required: true,
+ },
+ currentFlagPosition: {
+ type: Number,
+ required: true,
+ },
+ currentData: {
+ type: Object,
+ required: true,
+ },
+ graphHeight: {
+ type: Number,
+ required: true,
+ },
+ graphHeightOffset: {
+ type: Number,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ circleColorRgb: '#8fbce8',
+ };
+ },
+
+ computed: {
+ formatTime() {
+ return timeFormat(this.currentData.time);
+ },
+
+ formatDate() {
+ return dateFormat(this.currentData.time);
+ },
+
+ calculatedHeight() {
+ return this.graphHeight - this.graphHeightOffset;
+ },
+ },
+ };
+</script>
+<template>
+ <g class="mouse-over-flag">
+ <line
+ class="selected-metric-line"
+ :x1="currentXCoordinate"
+ :y1="0"
+ :x2="currentXCoordinate"
+ :y2="calculatedHeight"
+ transform="translate(-5, 20)">
+ </line>
+ <svg
+ class="rect-text-metric"
+ :x="currentFlagPosition"
+ y="0">
+ <rect
+ class="rect-metric"
+ x="4"
+ y="1"
+ rx="2"
+ width="90"
+ height="40"
+ transform="translate(-3, 20)">
+ </rect>
+ <text
+ class="text-metric text-metric-bold"
+ x="16"
+ y="35"
+ transform="translate(-5, 20)">
+ {{formatTime}}
+ </text>
+ <text
+ class="text-metric"
+ x="16"
+ y="15"
+ transform="translate(-5, 20)">
+ {{formatDate}}
+ </text>
+ </svg>
+ </g>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
new file mode 100644
index 00000000000..a43dad8e601
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -0,0 +1,179 @@
+<script>
+ import { formatRelevantDigits } from '../../../lib/utils/number_utils';
+
+ export default {
+ props: {
+ graphWidth: {
+ type: Number,
+ required: true,
+ },
+ graphHeight: {
+ type: Number,
+ required: true,
+ },
+ margin: {
+ type: Object,
+ required: true,
+ },
+ measurements: {
+ type: Object,
+ required: true,
+ },
+ legendTitle: {
+ type: String,
+ required: true,
+ },
+ yAxisLabel: {
+ type: String,
+ required: true,
+ },
+ timeSeries: {
+ type: Array,
+ required: true,
+ },
+ unitOfDisplay: {
+ type: String,
+ required: true,
+ },
+ currentDataIndex: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ yLabelWidth: 0,
+ yLabelHeight: 0,
+ seriesXPosition: 0,
+ metricUsageXPosition: 0,
+ };
+ },
+ computed: {
+ textTransform() {
+ const yCoordinate = (((this.graphHeight - this.margin.top)
+ + this.measurements.axisLabelLineOffset) / 2) || 0;
+
+ return `translate(15, ${yCoordinate}) rotate(-90)`;
+ },
+
+ rectTransform() {
+ const yCoordinate = ((this.graphHeight - this.margin.top) / 2)
+ + (this.yLabelWidth / 2) + 10 || 0;
+
+ return `translate(0, ${yCoordinate}) rotate(-90)`;
+ },
+
+ xPosition() {
+ return (((this.graphWidth + this.measurements.axisLabelLineOffset) / 2)
+ - this.margin.right) || 0;
+ },
+
+ yPosition() {
+ return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0;
+ },
+
+ },
+ methods: {
+ translateLegendGroup(index) {
+ return `translate(0, ${12 * (index)})`;
+ },
+
+ formatMetricUsage(series) {
+ return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`;
+ },
+ },
+ mounted() {
+ this.$nextTick(() => {
+ const bbox = this.$refs.ylabel.getBBox();
+ this.metricUsageXPosition = 0;
+ this.seriesXPosition = 0;
+ if (this.$refs.legendTitleSvg != null) {
+ this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
+ }
+ if (this.$refs.seriesTitleSvg != null) {
+ this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
+ }
+ this.yLabelWidth = bbox.width + 10; // Added some padding
+ this.yLabelHeight = bbox.height + 5;
+ });
+ },
+ };
+</script>
+<template>
+ <g
+ class="axis-label-container">
+ <line
+ class="label-x-axis-line"
+ stroke="#000000"
+ stroke-width="1"
+ x1="10"
+ :y1="yPosition"
+ :x2="graphWidth + 20"
+ :y2="yPosition">
+ </line>
+ <line
+ class="label-y-axis-line"
+ stroke="#000000"
+ stroke-width="1"
+ x1="10"
+ y1="0"
+ :x2="10"
+ :y2="yPosition">
+ </line>
+ <rect
+ class="rect-axis-text"
+ :transform="rectTransform"
+ :width="yLabelWidth"
+ :height="yLabelHeight">
+ </rect>
+ <text
+ class="label-axis-text y-label-text"
+ text-anchor="middle"
+ :transform="textTransform"
+ ref="ylabel">
+ {{yAxisLabel}}
+ </text>
+ <rect
+ class="rect-axis-text"
+ :x="xPosition + 60"
+ :y="graphHeight - 80"
+ width="35"
+ height="50">
+ </rect>
+ <text
+ class="label-axis-text x-label-text"
+ :x="xPosition + 60"
+ :y="yPosition"
+ dy=".35em">
+ Time
+ </text>
+ <g class="legend-group"
+ v-for="(series, index) in timeSeries"
+ :key="index"
+ :transform="translateLegendGroup(index)">
+ <rect
+ :fill="series.areaColor"
+ :width="measurements.legends.width"
+ :height="measurements.legends.height"
+ x="20"
+ :y="graphHeight - measurements.legendOffset">
+ </rect>
+ <text
+ v-if="timeSeries.length > 1"
+ class="legend-metric-title"
+ ref="legendTitleSvg"
+ x="38"
+ :y="graphHeight - 30">
+ {{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}}
+ </text>
+ <text
+ v-else
+ class="legend-metric-title"
+ ref="legendTitleSvg"
+ x="38"
+ :y="graphHeight - 30">
+ {{legendTitle}} {{formatMetricUsage(series)}}
+ </text>
+ </g>
+ </g>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
new file mode 100644
index 00000000000..958f537d31b
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -0,0 +1,21 @@
+<script>
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="panel panel-default prometheus-panel">
+ <div class="panel-heading">
+ <h4>{{name}}</h4>
+ </div>
+ <div class="panel-body prometheus-graph-group">
+ <slot />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/monitoring.vue b/app/assets/javascripts/monitoring/components/monitoring.vue
deleted file mode 100644
index a6a2d3119e3..00000000000
--- a/app/assets/javascripts/monitoring/components/monitoring.vue
+++ /dev/null
@@ -1,157 +0,0 @@
-<script>
- /* global Flash */
- import _ from 'underscore';
- import statusCodes from '../../lib/utils/http_status';
- import MonitoringService from '../services/monitoring_service';
- import monitoringRow from './monitoring_row.vue';
- import monitoringState from './monitoring_state.vue';
- import MonitoringStore from '../stores/monitoring_store';
- import eventHub from '../event_hub';
-
- export default {
-
- data() {
- const metricsData = document.querySelector('#prometheus-graphs').dataset;
- const store = new MonitoringStore();
-
- return {
- store,
- state: 'gettingStarted',
- hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics),
- documentationPath: metricsData.documentationPath,
- settingsPath: metricsData.settingsPath,
- endpoint: metricsData.additionalMetrics,
- deploymentEndpoint: metricsData.deploymentEndpoint,
- showEmptyState: true,
- backOffRequestCounter: 0,
- updateAspectRatio: false,
- updatedAspectRatios: 0,
- resizeThrottled: {},
- };
- },
-
- components: {
- monitoringRow,
- monitoringState,
- },
-
- 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.'));
- },
-
- resize() {
- this.updateAspectRatio = true;
- },
-
- toggleAspectRatio() {
- this.updatedAspectRatios = this.updatedAspectRatios += 1;
- if (this.store.getMetricsCount() === this.updatedAspectRatios) {
- this.updateAspectRatio = !this.updateAspectRatio;
- this.updatedAspectRatios = 0;
- }
- },
-
- },
-
- created() {
- this.service = new MonitoringService(this.endpoint);
- eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
- },
-
- beforeDestroy() {
- eventHub.$off('toggleAspectRatio', this.toggleAspectRatio);
- window.removeEventListener('resize', this.resizeThrottled, false);
- },
-
- mounted() {
- this.resizeThrottled = _.throttle(this.resize, 600);
- if (!this.hasMetrics) {
- this.state = 'gettingStarted';
- } else {
- this.getGraphsData();
- window.addEventListener('resize', this.resizeThrottled, false);
- }
- },
- };
-</script>
-<template>
- <div
- class="prometheus-graphs"
- v-if="!showEmptyState">
- <div
- class="row"
- v-for="(groupData, index) in store.groups"
- :key="index">
- <div
- class="col-md-12">
- <div
- class="panel panel-default prometheus-panel">
- <div
- class="panel-heading">
- <h4>{{groupData.group}}</h4>
- </div>
- <div
- class="panel-body">
- <monitoring-row
- v-for="(row, index) in groupData.metrics"
- :key="index"
- :row-data="row"
- :update-aspect-ratio="updateAspectRatio"
- :deployment-data="store.deploymentData"
- />
- </div>
- </div>
- </div>
- </div>
- </div>
- <monitoring-state
- :selected-state="state"
- :documentation-path="documentationPath"
- :settings-path="settingsPath"
- v-else
- />
-</template>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_column.vue b/app/assets/javascripts/monitoring/components/monitoring_column.vue
deleted file mode 100644
index c376baea79c..00000000000
--- a/app/assets/javascripts/monitoring/components/monitoring_column.vue
+++ /dev/null
@@ -1,297 +0,0 @@
-<script>
- /* global Breakpoints */
- import d3 from 'd3';
- import monitoringLegends from './monitoring_legends.vue';
- import monitoringFlag from './monitoring_flag.vue';
- import monitoringDeployment from './monitoring_deployment.vue';
- import MonitoringMixin from '../mixins/monitoring_mixins';
- import eventHub from '../event_hub';
- import measurements from '../utils/measurements';
- import { formatRelevantDigits } from '../../lib/utils/number_utils';
-
- const bisectDate = d3.bisector(d => d.time).left;
-
- export default {
- props: {
- columnData: {
- type: Object,
- required: true,
- },
- classType: {
- type: String,
- required: true,
- },
- updateAspectRatio: {
- type: Boolean,
- required: true,
- },
- deploymentData: {
- type: Array,
- required: true,
- },
- },
-
- mixins: [MonitoringMixin],
-
- data() {
- return {
- graphHeight: 450,
- graphWidth: 600,
- graphHeightOffset: 120,
- xScale: {},
- yScale: {},
- margin: {},
- data: [],
- breakpointHandler: Breakpoints.get(),
- unitOfDisplay: '',
- areaColorRgb: '#8fbce8',
- lineColorRgb: '#1f78d1',
- yAxisLabel: '',
- legendTitle: '',
- reducedDeploymentData: [],
- area: '',
- line: '',
- measurements: measurements.large,
- currentData: {
- time: new Date(),
- value: 0,
- },
- currentYCoordinate: 0,
- currentXCoordinate: 0,
- currentFlagPosition: 0,
- metricUsage: '',
- showFlag: false,
- showDeployInfo: true,
- };
- },
-
- components: {
- monitoringLegends,
- monitoringFlag,
- monitoringDeployment,
- },
-
- computed: {
- outterViewBox() {
- return `0 0 ${this.graphWidth} ${this.graphHeight}`;
- },
-
- innerViewBox() {
- if ((this.graphWidth - 150) > 0) {
- return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`;
- }
- return '0 0 0 0';
- },
-
- axisTransform() {
- return `translate(70, ${this.graphHeight - 100})`;
- },
-
- paddingBottomRootSvg() {
- return {
- paddingBottom: `${(Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0}%`,
- };
- },
- },
-
- methods: {
- draw() {
- const breakpointSize = this.breakpointHandler.getBreakpointSize();
- const query = this.columnData.queries[0];
- this.margin = measurements.large.margin;
- if (breakpointSize === 'xs' || breakpointSize === 'sm') {
- this.graphHeight = 300;
- this.margin = measurements.small.margin;
- this.measurements = measurements.small;
- }
- this.data = query.result[0].values;
- this.unitOfDisplay = query.unit || '';
- this.yAxisLabel = this.columnData.y_label || 'Values';
- this.legendTitle = query.label || 'Average';
- this.graphWidth = this.$refs.baseSvg.clientWidth -
- this.margin.left - this.margin.right;
- this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
- if (this.data !== undefined) {
- this.renderAxesPaths();
- this.formatDeployments();
- }
- },
-
- handleMouseOverGraph(e) {
- let point = this.$refs.graphData.createSVGPoint();
- point.x = e.clientX;
- point.y = e.clientY;
- point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
- point.x = point.x += 7;
- const timeValueOverlay = this.xScale.invert(point.x);
- const overlayIndex = bisectDate(this.data, timeValueOverlay, 1);
- const d0 = this.data[overlayIndex - 1];
- const d1 = this.data[overlayIndex];
- if (d0 === undefined || d1 === undefined) return;
- const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
- this.currentData = evalTime ? d1 : d0;
- this.currentXCoordinate = Math.floor(this.xScale(this.currentData.time));
- const currentDeployXPos = this.mouseOverDeployInfo(point.x);
- this.currentYCoordinate = this.yScale(this.currentData.value);
-
- 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;
- }
-
- this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`;
- },
-
- renderAxesPaths() {
- const axisXScale = d3.time.scale()
- .range([0, this.graphWidth]);
- this.yScale = d3.scale.linear()
- .range([this.graphHeight - this.graphHeightOffset, 0]);
- axisXScale.domain(d3.extent(this.data, d => d.time));
- this.yScale.domain([0, d3.max(this.data.map(d => d.value))]);
-
- const xAxis = d3.svg.axis()
- .scale(axisXScale)
- .ticks(measurements.xTicks)
- .orient('bottom');
-
- const yAxis = d3.svg.axis()
- .scale(this.yScale)
- .ticks(measurements.yTicks)
- .orient('left');
-
- d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
-
- const width = this.graphWidth;
- d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis)
- .selectAll('.tick')
- .each(function createTickLines(d, i) {
- if (i > 0) {
- d3.select(this).select('line')
- .attr('x2', width)
- .attr('class', 'axis-tick');
- } // Avoid adding the class to the first tick, to prevent coloring
- }); // This will select all of the ticks once they're rendered
-
- this.xScale = d3.time.scale()
- .range([0, this.graphWidth - 70]);
-
- this.xScale.domain(d3.extent(this.data, d => d.time));
-
- const areaFunction = d3.svg.area()
- .x(d => this.xScale(d.time))
- .y0(this.graphHeight - this.graphHeightOffset)
- .y1(d => this.yScale(d.value))
- .interpolate('linear');
-
- const lineFunction = d3.svg.line()
- .x(d => this.xScale(d.time))
- .y(d => this.yScale(d.value));
-
- this.line = lineFunction(this.data);
-
- this.area = areaFunction(this.data);
- },
- },
-
- watch: {
- updateAspectRatio() {
- if (this.updateAspectRatio) {
- this.graphHeight = 450;
- this.graphWidth = 600;
- this.measurements = measurements.large;
- this.draw();
- eventHub.$emit('toggleAspectRatio');
- }
- },
- },
-
- mounted() {
- this.draw();
- },
- };
-</script>
-<template>
- <div
- :class="classType">
- <h5
- class="text-center graph-title">
- {{columnData.title}}
- </h5>
- <div
- class="prometheus-svg-container"
- :style="paddingBottomRootSvg">
- <svg
- :viewBox="outterViewBox"
- ref="baseSvg">
- <g
- class="x-axis"
- :transform="axisTransform">
- </g>
- <g
- class="y-axis"
- transform="translate(70, 20)">
- </g>
- <monitoring-legends
- :graph-width="graphWidth"
- :graph-height="graphHeight"
- :margin="margin"
- :measurements="measurements"
- :area-color-rgb="areaColorRgb"
- :legend-title="legendTitle"
- :y-axis-label="yAxisLabel"
- :metric-usage="metricUsage"
- />
- <svg
- class="graph-data"
- :viewBox="innerViewBox"
- ref="graphData">
- <path
- class="metric-area"
- :d="area"
- :fill="areaColorRgb"
- transform="translate(-5, 20)">
- </path>
- <path
- class="metric-line"
- :d="line"
- :stroke="lineColorRgb"
- fill="none"
- stroke-width="2"
- transform="translate(-5, 20)">
- </path>
- <rect
- class="prometheus-graph-overlay"
- :width="(graphWidth - 70)"
- :height="(graphHeight - 100)"
- transform="translate(-5, 20)"
- ref="graphOverlay"
- @mousemove="handleMouseOverGraph($event)">
- </rect>
- <monitoring-deployment
- :show-deploy-info="showDeployInfo"
- :deployment-data="reducedDeploymentData"
- :graph-height="graphHeight"
- :graph-height-offset="graphHeightOffset"
- />
- <monitoring-flag
- v-if="showFlag"
- :current-x-coordinate="currentXCoordinate"
- :current-y-coordinate="currentYCoordinate"
- :current-data="currentData"
- :current-flag-position="currentFlagPosition"
- :graph-height="graphHeight"
- :graph-height-offset="graphHeightOffset"
- />
- </svg>
- </svg>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_deployment.vue b/app/assets/javascripts/monitoring/components/monitoring_deployment.vue
deleted file mode 100644
index e6432ba3191..00000000000
--- a/app/assets/javascripts/monitoring/components/monitoring_deployment.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-<script>
- import {
- dateFormat,
- timeFormat,
- } from '../constants';
-
- export default {
- props: {
- showDeployInfo: {
- type: Boolean,
- required: true,
- },
- deploymentData: {
- type: Array,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- graphHeightOffset: {
- type: Number,
- required: true,
- },
- },
-
- computed: {
- calculatedHeight() {
- return this.graphHeight - this.graphHeightOffset;
- },
- },
-
- methods: {
- refText(d) {
- return d.tag ? d.ref : d.sha.slice(0, 6);
- },
-
- formatTime(deploymentTime) {
- return timeFormat(deploymentTime);
- },
-
- formatDate(deploymentTime) {
- return dateFormat(deploymentTime);
- },
-
- nameDeploymentClass(deployment) {
- return `deploy-info-${deployment.id}`;
- },
-
- transformDeploymentGroup(deployment) {
- return `translate(${Math.floor(deployment.xPos) + 1}, 20)`;
- },
- },
- };
-</script>
-<template>
- <g
- class="deploy-info"
- v-if="showDeployInfo">
- <g
- v-for="(deployment, index) in deploymentData"
- :key="index"
- :class="nameDeploymentClass(deployment)"
- :transform="transformDeploymentGroup(deployment)">
- <rect
- x="0"
- y="0"
- :height="calculatedHeight"
- width="3"
- fill="url(#shadow-gradient)">
- </rect>
- <line
- class="deployment-line"
- x1="0"
- y1="0"
- x2="0"
- :y2="calculatedHeight"
- stroke="#000">
- </line>
- <svg
- v-if="deployment.showDeploymentFlag"
- class="js-deploy-info-box"
- x="3"
- y="0"
- width="92"
- height="60">
- <rect
- class="rect-text-metric deploy-info-rect rect-metric"
- x="1"
- y="1"
- rx="2"
- width="90"
- height="58">
- </rect>
- <g
- transform="translate(5, 2)">
- <text
- class="deploy-info-text text-metric-bold">
- {{refText(deployment)}}
- </text>
- </g>
- <text
- class="deploy-info-text"
- y="18"
- transform="translate(5, 2)">
- {{formatDate(deployment.time)}}
- </text>
- <text
- class="deploy-info-text text-metric-bold"
- y="38"
- transform="translate(5, 2)">
- {{formatTime(deployment.time)}}
- </text>
- </svg>
- </g>
- <svg
- height="0"
- width="0">
- <defs>
- <linearGradient
- id="shadow-gradient">
- <stop
- offset="0%"
- stop-color="#000"
- stop-opacity="0.4">
- </stop>
- <stop
- offset="100%"
- stop-color="#000"
- stop-opacity="0">
- </stop>
- </linearGradient>
- </defs>
- </svg>
- </g>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_flag.vue b/app/assets/javascripts/monitoring/components/monitoring_flag.vue
deleted file mode 100644
index 5a0e50fcab3..00000000000
--- a/app/assets/javascripts/monitoring/components/monitoring_flag.vue
+++ /dev/null
@@ -1,104 +0,0 @@
-<script>
- import {
- dateFormat,
- timeFormat,
- } from '../constants';
-
- export default {
- props: {
- currentXCoordinate: {
- type: Number,
- required: true,
- },
- currentYCoordinate: {
- type: Number,
- required: true,
- },
- currentFlagPosition: {
- type: Number,
- required: true,
- },
- currentData: {
- type: Object,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- graphHeightOffset: {
- type: Number,
- required: true,
- },
- },
-
- data() {
- return {
- circleColorRgb: '#8fbce8',
- };
- },
-
- computed: {
- formatTime() {
- return timeFormat(this.currentData.time);
- },
-
- formatDate() {
- return dateFormat(this.currentData.time);
- },
-
- calculatedHeight() {
- return this.graphHeight - this.graphHeightOffset;
- },
- },
- };
-</script>
-<template>
- <g class="mouse-over-flag">
- <line
- class="selected-metric-line"
- :x1="currentXCoordinate"
- :y1="0"
- :x2="currentXCoordinate"
- :y2="calculatedHeight"
- transform="translate(-5, 20)">
- </line>
- <circle
- class="circle-metric"
- :fill="circleColorRgb"
- stroke="#000"
- :cx="currentXCoordinate"
- :cy="currentYCoordinate"
- r="5"
- transform="translate(-5, 20)">
- </circle>
- <svg
- class="rect-text-metric"
- :x="currentFlagPosition"
- y="0">
- <rect
- class="rect-metric"
- x="4"
- y="1"
- rx="2"
- width="90"
- height="40"
- transform="translate(-3, 20)">
- </rect>
- <text
- class="text-metric text-metric-bold"
- x="16"
- y="35"
- transform="translate(-5, 20)">
- {{formatTime}}
- </text>
- <text
- class="text-metric"
- x="16"
- y="15"
- transform="translate(-5, 20)">
- {{formatDate}}
- </text>
- </svg>
- </g>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_legends.vue b/app/assets/javascripts/monitoring/components/monitoring_legends.vue
deleted file mode 100644
index 922a5e1bf0e..00000000000
--- a/app/assets/javascripts/monitoring/components/monitoring_legends.vue
+++ /dev/null
@@ -1,144 +0,0 @@
-<script>
- export default {
- props: {
- graphWidth: {
- type: Number,
- required: true,
- },
- graphHeight: {
- type: Number,
- required: true,
- },
- margin: {
- type: Object,
- required: true,
- },
- measurements: {
- type: Object,
- required: true,
- },
- areaColorRgb: {
- type: String,
- required: true,
- },
- legendTitle: {
- type: String,
- required: true,
- },
- yAxisLabel: {
- type: String,
- required: true,
- },
- metricUsage: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- yLabelWidth: 0,
- yLabelHeight: 0,
- };
- },
- computed: {
- textTransform() {
- const yCoordinate = (((this.graphHeight - this.margin.top)
- + this.measurements.axisLabelLineOffset) / 2) || 0;
-
- return `translate(15, ${yCoordinate}) rotate(-90)`;
- },
-
- rectTransform() {
- const yCoordinate = ((this.graphHeight - this.margin.top) / 2)
- + (this.yLabelWidth / 2) + 10 || 0;
-
- return `translate(0, ${yCoordinate}) rotate(-90)`;
- },
-
- xPosition() {
- return (((this.graphWidth + this.measurements.axisLabelLineOffset) / 2)
- - this.margin.right) || 0;
- },
-
- yPosition() {
- return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0;
- },
- },
- mounted() {
- this.$nextTick(() => {
- const bbox = this.$refs.ylabel.getBBox();
- this.yLabelWidth = bbox.width + 10; // Added some padding
- this.yLabelHeight = bbox.height + 5;
- });
- },
- };
-</script>
-<template>
- <g
- class="axis-label-container">
- <line
- class="label-x-axis-line"
- stroke="#000000"
- stroke-width="1"
- x1="10"
- :y1="yPosition"
- :x2="graphWidth + 20"
- :y2="yPosition">
- </line>
- <line
- class="label-y-axis-line"
- stroke="#000000"
- stroke-width="1"
- x1="10"
- y1="0"
- :x2="10"
- :y2="yPosition">
- </line>
- <rect
- class="rect-axis-text"
- :transform="rectTransform"
- :width="yLabelWidth"
- :height="yLabelHeight">
- </rect>
- <text
- class="label-axis-text y-label-text"
- text-anchor="middle"
- :transform="textTransform"
- ref="ylabel">
- {{yAxisLabel}}
- </text>
- <rect
- class="rect-axis-text"
- :x="xPosition + 60"
- :y="graphHeight - 80"
- width="35"
- height="50">
- </rect>
- <text
- class="label-axis-text x-label-text"
- :x="xPosition + 60"
- :y="yPosition"
- dy=".35em">
- Time
- </text>
- <rect
- :fill="areaColorRgb"
- :width="measurements.legends.width"
- :height="measurements.legends.height"
- x="20"
- :y="graphHeight - measurements.legendOffset">
- </rect>
- <text
- class="text-metric-title"
- x="50"
- :y="graphHeight - 25">
- {{legendTitle}}
- </text>
- <text
- class="text-metric-usage"
- x="50"
- :y="graphHeight - 10">
- {{metricUsage}}
- </text>
- </g>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_paths.vue b/app/assets/javascripts/monitoring/components/monitoring_paths.vue
new file mode 100644
index 00000000000..043f1bf66bb
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/monitoring_paths.vue
@@ -0,0 +1,40 @@
+<script>
+ export default {
+ props: {
+ generatedLinePath: {
+ type: String,
+ required: true,
+ },
+ generatedAreaPath: {
+ type: String,
+ required: true,
+ },
+ lineColor: {
+ type: String,
+ required: true,
+ },
+ areaColor: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+</script>
+<template>
+ <g>
+ <path
+ class="metric-area"
+ :d="generatedAreaPath"
+ :fill="areaColor"
+ transform="translate(-5, 20)">
+ </path>
+ <path
+ class="metric-line"
+ :d="generatedLinePath"
+ :stroke="lineColor"
+ fill="none"
+ stroke-width="1"
+ transform="translate(-5, 20)">
+ </path>
+ </g>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_row.vue b/app/assets/javascripts/monitoring/components/monitoring_row.vue
deleted file mode 100644
index e5528f17880..00000000000
--- a/app/assets/javascripts/monitoring/components/monitoring_row.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
- import monitoringColumn from './monitoring_column.vue';
-
- export default {
- props: {
- rowData: {
- type: Array,
- required: true,
- },
- updateAspectRatio: {
- type: Boolean,
- required: true,
- },
- deploymentData: {
- type: Array,
- required: true,
- },
- },
- components: {
- monitoringColumn,
- },
- computed: {
- bootstrapClass() {
- return this.rowData.length >= 2 ? 'col-md-6' : 'col-md-12';
- },
- },
- };
-</script>
-<template>
- <div
- class="prometheus-row row">
- <monitoring-column
- v-for="(column, index) in rowData"
- :column-data="column"
- :class-type="bootstrapClass"
- :key="index"
- :update-aspect-ratio="updateAspectRatio"
- :deployment-data="deploymentData"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_state.vue b/app/assets/javascripts/monitoring/components/monitoring_state.vue
deleted file mode 100644
index 598021aa4df..00000000000
--- a/app/assets/javascripts/monitoring/components/monitoring_state.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<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: {
- type: String,
- required: true,
- },
- settingsPath: {
- type: String,
- required: false,
- default: '',
- },
- selectedState: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- states: {
- gettingStarted: {
- svg: gettingStartedSvg,
- 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,
- 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,
- title: 'Unable to connect to Prometheus server',
- description: 'Ensure connectivity is available from the GitLab server to the ',
- buttonText: 'View documentation',
- },
- },
- };
- },
- computed: {
- currentState() {
- return this.states[this.selectedState];
- },
-
- buttonPath() {
- if (this.selectedState === 'gettingStarted') {
- return this.settingsPath;
- }
- return this.documentationPath;
- },
-
- showButtonDescription() {
- if (this.selectedState === 'unableToConnect') return true;
- return false;
- },
- },
- };
-</script>
-<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
- :href="settingsPath"
- v-if="showButtonDescription">
- Prometheus server
- </a>
- </div>
- </div>
- </div>
- <div
- class="row state-button-section">
- <div
- class="col-md-4 col-md-offset-4 text-center state-button">
- <a
- class="btn btn-success"
- :href="buttonPath">
- {{currentState.buttonText}}
- </a>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
deleted file mode 100644
index c3a8da52404..00000000000
--- a/app/assets/javascripts/monitoring/constants.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import d3 from 'd3';
-
-export const dateFormat = d3.time.format('%b %d, %Y');
-export const timeFormat = d3.time.format('%H:%M%p');
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
index 8e62fa63f13..345a0b37a76 100644
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
@@ -21,9 +21,9 @@ const mixins = {
formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
- const xPos = Math.floor(this.xScale(time));
+ const xPos = Math.floor(this.timeSeries[0].timeSeriesScaleX(time));
- time.setSeconds(this.data[0].time.getSeconds());
+ time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
if (xPos >= 0) {
deploymentDataArray.push({
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index 5d5cb56af72..ef280e02092 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
-import Monitoring from './components/monitoring.vue';
+import Dashboard from './components/dashboard.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#prometheus-graphs',
components: {
- 'monitoring-dashboard': Monitoring,
+ Dashboard,
},
- render: createElement => createElement('monitoring-dashboard'),
+ render: createElement => createElement('dashboard'),
}));
diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js
index 737c964f12e..7592af5878e 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -1,46 +1,36 @@
import _ from 'underscore';
-class MonitoringStore {
+function sortMetrics(metrics) {
+ return _.chain(metrics).sortBy('weight').sortBy('title').value();
+}
+
+function normalizeMetrics(metrics) {
+ return metrics.map(metric => ({
+ ...metric,
+ queries: metric.queries.map(query => ({
+ ...query,
+ result: query.result.map(result => ({
+ ...result,
+ values: result.values.map(([timestamp, value]) => ({
+ time: new Date(timestamp * 1000),
+ value,
+ })),
+ })),
+ })),
+ }));
+}
+
+export default class MonitoringStore {
constructor() {
this.groups = [];
this.deploymentData = [];
}
- // eslint-disable-next-line class-methods-use-this
- createArrayRows(metrics = []) {
- const currentMetrics = metrics;
- const availableMetrics = [];
- let metricsRow = [];
- let index = 1;
- Object.keys(currentMetrics).forEach((key) => {
- const metricValues = currentMetrics[key].queries[0].result[0].values;
- if (metricValues != null) {
- const literalMetrics = metricValues.map(metric => ({
- time: new Date(metric[0] * 1000),
- value: metric[1],
- }));
- currentMetrics[key].queries[0].result[0].values = literalMetrics;
- metricsRow.push(currentMetrics[key]);
- if (index % 2 === 0) {
- availableMetrics.push(metricsRow);
- metricsRow = [];
- }
- index = index += 1;
- }
- });
- if (metricsRow.length > 0) {
- availableMetrics.push(metricsRow);
- }
- return availableMetrics;
- }
-
storeMetrics(groups = []) {
- this.groups = groups.map((group) => {
- const currentGroup = group;
- currentGroup.metrics = _.chain(group.metrics).sortBy('weight').sortBy('title').value();
- currentGroup.metrics = this.createArrayRows(currentGroup.metrics);
- return currentGroup;
- });
+ this.groups = groups.map(group => ({
+ ...group,
+ metrics: normalizeMetrics(sortMetrics(group.metrics)),
+ }));
}
storeDeploymentData(deploymentData = []) {
@@ -48,14 +38,6 @@ class MonitoringStore {
}
getMetricsCount() {
- let metricsCount = 0;
- this.groups.forEach((group) => {
- group.metrics.forEach((metric) => {
- metricsCount = metricsCount += metric.length;
- });
- });
- return metricsCount;
+ return this.groups.reduce((count, group) => count + group.metrics.length, 0);
}
}
-
-export default MonitoringStore;
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
new file mode 100644
index 00000000000..26bcaa02511
--- /dev/null
+++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
@@ -0,0 +1,15 @@
+import d3 from 'd3';
+
+export const dateFormat = d3.time.format('%b %-d, %Y');
+export const timeFormat = d3.time.format('%-I:%M%p');
+
+export const timeScaleFormat = d3.time.format.multi([
+ ['.%L', d => d.getMilliseconds()],
+ [':%S', d => d.getSeconds()],
+ ['%-I:%M', d => d.getMinutes()],
+ ['%-I %p', d => d.getHours()],
+ ['%a %-d', d => d.getDay() && d.getDate() !== 1],
+ ['%b %-d', d => d.getDate() !== 1],
+ ['%B', d => d.getMonth()],
+ ['%Y', () => true],
+]);
diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js
index 62cd19c86e1..ee3c45efacc 100644
--- a/app/assets/javascripts/monitoring/utils/measurements.js
+++ b/app/assets/javascripts/monitoring/utils/measurements.js
@@ -7,15 +7,15 @@ export default {
left: 40,
},
legends: {
- width: 15,
- height: 25,
+ width: 10,
+ height: 3,
},
backgroundLegend: {
width: 30,
height: 50,
},
axisLabelLineOffset: -20,
- legendOffset: 35,
+ legendOffset: 33,
},
large: { // This covers both md and lg screen sizes
margin: {
@@ -25,15 +25,15 @@ export default {
left: 80,
},
legends: {
- width: 20,
- height: 30,
+ width: 15,
+ height: 3,
},
backgroundLegend: {
width: 30,
height: 150,
},
axisLabelLineOffset: 20,
- legendOffset: 38,
+ legendOffset: 36,
},
xTicks: 8,
yTicks: 3,
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
new file mode 100644
index 00000000000..05d551e917c
--- /dev/null
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -0,0 +1,80 @@
+import d3 from 'd3';
+import _ from 'underscore';
+
+export default function createTimeSeries(seriesData, graphWidth, graphHeight, graphHeightOffset) {
+ const maxValues = seriesData.map((timeSeries, index) => {
+ const maxValue = d3.max(timeSeries.values.map(d => d.value));
+ return {
+ maxValue,
+ index,
+ };
+ });
+
+ const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
+
+ let timeSeriesNumber = 1;
+ let lineColor = '#1f78d1';
+ let areaColor = '#8fbce8';
+ return seriesData.map((timeSeries) => {
+ const timeSeriesScaleX = d3.time.scale()
+ .range([0, graphWidth - 70]);
+
+ const timeSeriesScaleY = d3.scale.linear()
+ .range([graphHeight - graphHeightOffset, 0]);
+
+ timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time));
+ timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
+
+ const lineFunction = d3.svg.line()
+ .x(d => timeSeriesScaleX(d.time))
+ .y(d => timeSeriesScaleY(d.value));
+
+ const areaFunction = d3.svg.area()
+ .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;
+ }
+
+ if (timeSeriesNumber <= 5) {
+ timeSeriesNumber = timeSeriesNumber += 1;
+ } else {
+ timeSeriesNumber = 1;
+ }
+
+ return {
+ linePath: lineFunction(timeSeries.values),
+ areaPath: areaFunction(timeSeries.values),
+ timeSeriesScaleX,
+ values: timeSeries.values,
+ lineColor,
+ areaColor,
+ };
+ });
+}
diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js
index 930218dd1f5..709a5d33b9f 100644
--- a/app/assets/javascripts/new_sidebar.js
+++ b/app/assets/javascripts/new_sidebar.js
@@ -1,7 +1,6 @@
import Cookies from 'js-cookie';
import _ from 'underscore';
-/* global bp */
-import './breakpoints';
+import bp from './breakpoints';
export default class NewNavSidebar {
constructor() {
@@ -16,6 +15,7 @@ export default class NewNavSidebar {
this.$openSidebar = $('.toggle-mobile-nav');
this.$closeSidebar = $('.close-nav-button');
this.$sidebarToggle = $('.js-toggle-sidebar');
+ this.$topLevelLinks = $('.sidebar-top-level-items > li > a');
}
bindEvents() {
@@ -44,12 +44,17 @@ export default class NewNavSidebar {
}
toggleCollapsedSidebar(collapsed) {
- this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
+ const breakpoint = bp.getBreakpointSize();
+
if (this.$sidebar.length) {
- this.$page.toggleClass('page-with-new-sidebar', !collapsed);
- this.$page.toggleClass('page-with-icon-sidebar', collapsed);
+ this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
+ this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed);
}
NewNavSidebar.setCollapsedCookie(collapsed);
+
+ this.$topLevelLinks.attr('title', function updateTopLevelTitle() {
+ return collapsed ? this.getAttribute('aria-label') : '';
+ });
}
render() {
@@ -58,7 +63,7 @@ export default class NewNavSidebar {
if (breakpoint === 'sm' || breakpoint === 'md') {
this.toggleCollapsedSidebar(true);
} else if (breakpoint === 'lg') {
- const collapse = Cookies.get('sidebar_collapsed') === 'true';
+ const collapse = this.$sidebar.hasClass('sidebar-icons-only');
this.toggleCollapsedSidebar(collapse);
}
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index b38a6abc8d1..a09270d6d24 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -464,7 +464,6 @@ export default class Notes {
}
renderDiscussionAvatar(diffAvatarContainer, noteEntity) {
- var commentButton = diffAvatarContainer.find('.js-add-diff-note-button');
var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
if (!avatarHolder.length) {
@@ -475,10 +474,6 @@ export default class Notes {
gl.diffNotesCompileComponents();
}
-
- if (commentButton.length) {
- commentButton.remove();
- }
}
/**
@@ -767,6 +762,7 @@ export default class Notes {
var $note, $notes;
$note = $(el);
$notes = $note.closest('.discussion-notes');
+ const discussionId = $('.notes', $notes).data('discussion-id');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (gl.diffNoteApps[noteElId]) {
@@ -783,6 +779,8 @@ export default class Notes {
// "Discussions" tab
$notes.closest('.timeline-entry').remove();
+ $(`.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) {
$notes.remove();
diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue
new file mode 100644
index 00000000000..16f4e22aa9b
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_comment_form.vue
@@ -0,0 +1,347 @@
+<script>
+ /* global Flash, Autosave */
+ import { mapActions, mapGetters } from 'vuex';
+ import _ from 'underscore';
+ 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 issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
+ import markdownField from '../../vue_shared/components/markdown/field.vue';
+ import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+
+ export default {
+ name: 'issueCommentForm',
+ data() {
+ return {
+ note: '',
+ noteType: constants.COMMENT,
+ // Can't use mapGetters,
+ // this needs to be in the data object because it belongs to the state
+ issueState: this.$store.getters.getIssueData.state,
+ isSubmitting: false,
+ isSubmitButtonDisabled: true,
+ };
+ },
+ components: {
+ confidentialIssue,
+ issueNoteSignedOutWidget,
+ markdownField,
+ userAvatarLink,
+ },
+ watch: {
+ note(newNote) {
+ this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
+ },
+ isSubmitting(newValue) {
+ this.setIsSubmitButtonDisabled(this.note, newValue);
+ },
+ },
+ computed: {
+ ...mapGetters([
+ 'getCurrentUserLastNote',
+ 'getUserData',
+ 'getIssueData',
+ 'getNotesData',
+ ]),
+ isLoggedIn() {
+ return this.getUserData.id;
+ },
+ commentButtonTitle() {
+ return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
+ },
+ isIssueOpen() {
+ return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
+ },
+ issueActionButtonTitle() {
+ if (this.note.length) {
+ const actionText = this.isIssueOpen ? 'close' : 'reopen';
+
+ return this.noteType === constants.COMMENT ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`;
+ }
+
+ return this.isIssueOpen ? 'Close issue' : 'Reopen issue';
+ },
+ actionButtonClassNames() {
+ return {
+ 'btn-reopen': !this.isIssueOpen,
+ 'btn-close': this.isIssueOpen,
+ 'js-note-target-close': this.isIssueOpen,
+ 'js-note-target-reopen': !this.isIssueOpen,
+ };
+ },
+ markdownDocsPath() {
+ return this.getNotesData.markdownDocsPath;
+ },
+ quickActionsDocsPath() {
+ return this.getNotesData.quickActionsDocsPath;
+ },
+ markdownPreviewPath() {
+ return this.getIssueData.preview_note_path;
+ },
+ author() {
+ return this.getUserData;
+ },
+ canUpdateIssue() {
+ return this.getIssueData.current_user.can_update;
+ },
+ endpoint() {
+ return this.getIssueData.create_note_path;
+ },
+ isConfidentialIssue() {
+ return this.getIssueData.confidential;
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'saveNote',
+ 'removePlaceholderNotes',
+ ]),
+ setIsSubmitButtonDisabled(note, isSubmitting) {
+ if (!_.isEmpty(note) && !isSubmitting) {
+ this.isSubmitButtonDisabled = false;
+ } else {
+ this.isSubmitButtonDisabled = true;
+ }
+ },
+ handleSave(withIssueAction) {
+ if (this.note.length) {
+ const noteData = {
+ endpoint: this.endpoint,
+ flashContainer: this.$el,
+ data: {
+ note: {
+ noteable_type: constants.NOTEABLE_TYPE,
+ noteable_id: this.getIssueData.id,
+ note: this.note,
+ },
+ },
+ };
+
+ if (this.noteType === constants.DISCUSSION) {
+ noteData.data.note.type = constants.DISCUSSION_NOTE;
+ }
+ this.isSubmitting = true;
+ this.note = ''; // Empty textarea while being requested. Repopulate in catch
+
+ this.saveNote(noteData)
+ .then((res) => {
+ this.isSubmitting = false;
+ if (res.errors) {
+ if (res.errors.commands_only) {
+ this.discard();
+ } else {
+ Flash(
+ 'Something went wrong while adding your comment. Please try again.',
+ 'alert',
+ $(this.$refs.commentForm),
+ );
+ }
+ } else {
+ this.discard();
+ }
+
+ if (withIssueAction) {
+ this.toggleIssueState();
+ }
+ })
+ .catch(() => {
+ this.isSubmitting = false;
+ this.discard(false);
+ const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
+ Flash(msg, 'alert', $(this.$el));
+ this.note = noteData.data.note.note; // Restore textarea content.
+ this.removePlaceholderNotes();
+ });
+ } else {
+ this.toggleIssueState();
+ }
+ },
+ toggleIssueState() {
+ this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED;
+
+ // This is out of scope for the Notes Vue component.
+ // It was the shortest path to update the issue state and relevant places.
+ const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close';
+ $(`.js-btn-issue-action.${btnClass}:visible`).trigger('click');
+ },
+ discard(shouldClear = true) {
+ // `blur` is needed to clear slash commands autocomplete cache if event fired.
+ // `focus` is needed to remain cursor in the textarea.
+ this.$refs.textarea.blur();
+ this.$refs.textarea.focus();
+
+ if (shouldClear) {
+ this.note = '';
+ }
+
+ // reset autostave
+ this.autosave.reset();
+ },
+ setNoteType(type) {
+ this.noteType = type;
+ },
+ editCurrentUserLastNote() {
+ if (this.note === '') {
+ const lastNote = this.getCurrentUserLastNote;
+
+ if (lastNote) {
+ eventHub.$emit('enterEditMode', {
+ noteId: lastNote.id,
+ });
+ }
+ }
+ },
+ initAutoSave() {
+ if (this.isLoggedIn) {
+ this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue');
+ }
+ },
+ initTaskList() {
+ return new TaskList({
+ dataType: 'note',
+ fieldName: 'note',
+ selector: '.notes',
+ });
+ },
+ },
+ mounted() {
+ // jQuery is needed here because it is a custom event being dispatched with jQuery.
+ $(document).on('issuable:change', (e, isClosed) => {
+ this.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
+ });
+
+ this.initAutoSave();
+ this.initTaskList();
+ },
+ };
+</script>
+
+<template>
+ <div>
+ <issue-note-signed-out-widget v-if="!isLoggedIn" />
+ <ul
+ v-else
+ class="notes notes-form timeline">
+ <li class="timeline-entry">
+ <div class="timeline-entry-inner">
+ <div class="flash-container error-alert timeline-content"></div>
+ <div class="timeline-icon hidden-xs hidden-sm">
+ <user-avatar-link
+ v-if="author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ />
+ </div>
+ <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" />
+ <div class="error-alert"></div>
+ <markdown-field
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :add-spacing-classes="false"
+ :is-confidential-issue="isConfidentialIssue">
+ <textarea
+ id="note-body"
+ name="note[note]"
+ class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area js-vue-textarea"
+ data-supports-quick-actions="true"
+ aria-label="Description"
+ v-model="note"
+ ref="textarea"
+ slot="textarea"
+ placeholder="Write a comment or drag your files here..."
+ @keydown.up="editCurrentUserLastNote()"
+ @keydown.meta.enter="handleSave()">
+ </textarea>
+ </markdown-field>
+ <div class="note-form-actions">
+ <div class="pull-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown">
+ <button
+ @click.prevent="handleSave()"
+ :disabled="isSubmitButtonDisabled"
+ class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
+ type="submit">
+ {{commentButtonTitle}}
+ </button>
+ <button
+ :disabled="isSubmitButtonDisabled"
+ name="button"
+ type="button"
+ class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle"
+ data-toggle="dropdown"
+ aria-label="Open comment type dropdown">
+ <i
+ aria-hidden="true"
+ class="fa fa-caret-down toggle-icon">
+ </i>
+ </button>
+
+ <ul class="note-type-dropdown dropdown-open-top dropdown-menu">
+ <li :class="{ 'droplab-item-selected': noteType === 'comment' }">
+ <button
+ type="button"
+ class="btn btn-transparent"
+ @click.prevent="setNoteType('comment')">
+ <i
+ aria-hidden="true"
+ class="fa fa-check icon">
+ </i>
+ <div class="description">
+ <strong>Comment</strong>
+ <p>
+ Add a general comment to this issue.
+ </p>
+ </div>
+ </button>
+ </li>
+ <li class="divider droplab-item-ignore"></li>
+ <li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
+ <button
+ type="button"
+ class="btn btn-transparent"
+ @click.prevent="setNoteType('discussion')">
+ <i
+ aria-hidden="true"
+ class="fa fa-check icon">
+ </i>
+ <div class="description">
+ <strong>Start discussion</strong>
+ <p>
+ Discuss a specific suggestion or question.
+ </p>
+ </div>
+ </button>
+ </li>
+ </ul>
+ </div>
+ <button
+ type="button"
+ @click="handleSave(true)"
+ v-if="canUpdateIssue"
+ :class="actionButtonClassNames"
+ class="btn btn-comment btn-comment-and-close">
+ {{issueActionButtonTitle}}
+ </button>
+ <button
+ type="button"
+ v-if="note.length"
+ @click="discard"
+ class="btn btn-cancel js-note-discard">
+ Discard draft
+ </button>
+ </div>
+ </form>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/issue_discussion.vue
new file mode 100644
index 00000000000..b131ef4b182
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_discussion.vue
@@ -0,0 +1,232 @@
+<script>
+ /* global Flash */
+ import { mapActions, mapGetters } from 'vuex';
+ import { SYSTEM_NOTE } from '../constants';
+ import issueNote from './issue_note.vue';
+ import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+ import issueNoteHeader from './issue_note_header.vue';
+ import issueNoteActions from './issue_note_actions.vue';
+ import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
+ import issueNoteEditedText from './issue_note_edited_text.vue';
+ import issueNoteForm from './issue_note_form.vue';
+ import placeholderNote from './issue_placeholder_note.vue';
+ import placeholderSystemNote from './issue_placeholder_system_note.vue';
+ import autosave from '../mixins/autosave';
+
+ export default {
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isReplying: false,
+ };
+ },
+ components: {
+ issueNote,
+ userAvatarLink,
+ issueNoteHeader,
+ issueNoteActions,
+ issueNoteSignedOutWidget,
+ issueNoteEditedText,
+ issueNoteForm,
+ placeholderNote,
+ placeholderSystemNote,
+ },
+ mixins: [
+ autosave,
+ ],
+ computed: {
+ ...mapGetters([
+ 'getIssueData',
+ ]),
+ discussion() {
+ return this.note.notes[0];
+ },
+ author() {
+ return this.discussion.author;
+ },
+ canReply() {
+ return this.getIssueData.current_user.can_create_note;
+ },
+ newNotePath() {
+ return this.getIssueData.create_note_path;
+ },
+ lastUpdatedBy() {
+ const { notes } = this.note;
+
+ if (notes.length > 1) {
+ return notes[notes.length - 1].author;
+ }
+
+ return null;
+ },
+ lastUpdatedAt() {
+ const { notes } = this.note;
+
+ if (notes.length > 1) {
+ return notes[notes.length - 1].created_at;
+ }
+
+ return null;
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'saveNote',
+ 'toggleDiscussion',
+ 'removePlaceholderNotes',
+ ]),
+ componentName(note) {
+ if (note.isPlaceholderNote) {
+ if (note.placeholderType === SYSTEM_NOTE) {
+ return placeholderSystemNote;
+ }
+ return placeholderNote;
+ }
+
+ return issueNote;
+ },
+ componentData(note) {
+ return note.isPlaceholderNote ? note.notes[0] : note;
+ },
+ toggleDiscussionHandler() {
+ this.toggleDiscussion({ discussionId: this.note.id });
+ },
+ showReplyForm() {
+ this.isReplying = true;
+ },
+ cancelReplyForm(shouldConfirm) {
+ if (shouldConfirm && this.$refs.noteForm.isDirty) {
+ // eslint-disable-next-line no-alert
+ if (!confirm('Are you sure you want to cancel creating this comment?')) {
+ return;
+ }
+ }
+
+ this.resetAutoSave();
+ this.isReplying = false;
+ },
+ saveReply(noteText, form, callback) {
+ const replyData = {
+ endpoint: this.newNotePath,
+ flashContainer: this.$el,
+ data: {
+ in_reply_to_discussion_id: this.note.reply_id,
+ target_type: 'issue',
+ target_id: this.discussion.noteable_id,
+ note: { note: noteText },
+ },
+ };
+ this.isReplying = false;
+
+ this.saveNote(replyData)
+ .then(() => {
+ this.resetAutoSave();
+ callback();
+ })
+ .catch((err) => {
+ this.removePlaceholderNotes();
+ this.isReplying = true;
+ this.$nextTick(() => {
+ const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
+ Flash(msg, 'alert', $(this.$el));
+ this.$refs.noteForm.note = noteText;
+ callback(err);
+ });
+ });
+ },
+ },
+ mounted() {
+ if (this.isReplying) {
+ this.initAutoSave();
+ }
+ },
+ updated() {
+ if (this.isReplying) {
+ if (!this.autosave) {
+ this.initAutoSave();
+ } else {
+ this.setAutoSave();
+ }
+ }
+ },
+ };
+</script>
+
+<template>
+ <li class="note note-discussion timeline-entry">
+ <div class="timeline-entry-inner">
+ <div class="timeline-icon">
+ <user-avatar-link
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ />
+ </div>
+ <div class="timeline-content">
+ <div class="discussion">
+ <div class="discussion-header">
+ <issue-note-header
+ :author="author"
+ :created-at="discussion.created_at"
+ :note-id="discussion.id"
+ :include-toggle="true"
+ @toggleHandler="toggleDiscussionHandler"
+ action-text="started a discussion"
+ class="discussion"
+ />
+ <issue-note-edited-text
+ v-if="lastUpdatedAt"
+ :edited-at="lastUpdatedAt"
+ :edited-by="lastUpdatedBy"
+ action-text="Last updated"
+ class-name="discussion-headline-light js-discussion-headline"
+ />
+ </div>
+ </div>
+ <div
+ v-if="note.expanded"
+ class="discussion-body">
+ <div class="panel panel-default">
+ <div class="discussion-notes">
+ <ul class="notes">
+ <component
+ v-for="note in note.notes"
+ :is="componentName(note)"
+ :note="componentData(note)"
+ :key="note.id"
+ />
+ </ul>
+ <div
+ :class="{ 'is-replying': isReplying }"
+ class="discussion-reply-holder">
+ <button
+ v-if="canReply && !isReplying"
+ @click="showReplyForm"
+ type="button"
+ class="js-vue-discussion-reply btn btn-text-field"
+ title="Add a reply">Reply...</button>
+ <issue-note-form
+ v-if="isReplying"
+ save-button-title="Comment"
+ :discussion="note"
+ :is-editing="false"
+ @handleFormUpdate="saveReply"
+ @cancelFormEdition="cancelReplyForm"
+ ref="noteForm"
+ />
+ <issue-note-signed-out-widget v-if="!canReply" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue
new file mode 100644
index 00000000000..3483f6c7538
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_note.vue
@@ -0,0 +1,186 @@
+<script>
+ /* global Flash */
+
+ import { mapGetters, mapActions } from 'vuex';
+ import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+ import issueNoteHeader from './issue_note_header.vue';
+ import issueNoteActions from './issue_note_actions.vue';
+ import issueNoteBody from './issue_note_body.vue';
+ import eventHub from '../event_hub';
+
+ export default {
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isEditing: false,
+ isDeleting: false,
+ isRequesting: false,
+ };
+ },
+ components: {
+ userAvatarLink,
+ issueNoteHeader,
+ issueNoteActions,
+ issueNoteBody,
+ },
+ computed: {
+ ...mapGetters([
+ 'targetNoteHash',
+ 'getUserData',
+ ]),
+ author() {
+ return this.note.author;
+ },
+ classNameBindings() {
+ return {
+ 'is-editing': this.isEditing && !this.isRequesting,
+ 'is-requesting being-posted': this.isRequesting,
+ 'disabled-content': this.isDeleting,
+ target: this.targetNoteHash === this.noteAnchorId,
+ };
+ },
+ canReportAsAbuse() {
+ return this.note.report_abuse_path && this.author.id !== this.getUserData.id;
+ },
+ noteAnchorId() {
+ return `note_${this.note.id}`;
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'deleteNote',
+ 'updateNote',
+ 'scrollToNoteIfNeeded',
+ ]),
+ editHandler() {
+ this.isEditing = true;
+ },
+ deleteHandler() {
+ // eslint-disable-next-line no-alert
+ if (confirm('Are you sure you want to delete this list?')) {
+ this.isDeleting = true;
+
+ this.deleteNote(this.note)
+ .then(() => {
+ this.isDeleting = false;
+ })
+ .catch(() => {
+ Flash('Something went wrong while deleting your note. Please try again.');
+ this.isDeleting = false;
+ });
+ }
+ },
+ formUpdateHandler(noteText, parentElement, callback) {
+ const data = {
+ endpoint: this.note.path,
+ note: {
+ target_type: 'issue',
+ target_id: this.note.noteable_id,
+ note: { note: noteText },
+ },
+ };
+ this.isRequesting = true;
+ this.oldContent = this.note.note_html;
+ this.note.note_html = noteText;
+
+ this.updateNote(data)
+ .then(() => {
+ this.isEditing = false;
+ this.isRequesting = false;
+ $(this.$refs.noteBody.$el).renderGFM();
+ this.$refs.noteBody.resetAutoSave();
+ callback();
+ })
+ .catch(() => {
+ this.isRequesting = false;
+ this.isEditing = true;
+ this.$nextTick(() => {
+ const msg = 'Something went wrong while editing your comment. Please try again.';
+ Flash(msg, 'alert', $(this.$el));
+ this.recoverNoteContent(noteText);
+ callback();
+ });
+ });
+ },
+ formCancelHandler(shouldConfirm, isDirty) {
+ if (shouldConfirm && isDirty) {
+ // eslint-disable-next-line no-alert
+ if (!confirm('Are you sure you want to cancel editing this comment?')) return;
+ }
+ this.$refs.noteBody.resetAutoSave();
+ if (this.oldContent) {
+ this.note.note_html = this.oldContent;
+ this.oldContent = null;
+ }
+ this.isEditing = false;
+ },
+ recoverNoteContent(noteText) {
+ // we need to do this to prevent noteForm inconsistent content warning
+ // this is something we intentionally do so we need to recover the content
+ this.note.note = noteText;
+ this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better
+ },
+ },
+ created() {
+ eventHub.$on('enterEditMode', ({ noteId }) => {
+ if (noteId === this.note.id) {
+ this.isEditing = true;
+ this.scrollToNoteIfNeeded($(this.$el));
+ }
+ });
+ },
+ };
+</script>
+
+<template>
+ <li
+ class="note timeline-entry"
+ :id="noteAnchorId"
+ :class="classNameBindings"
+ :data-award-url="note.toggle_award_path">
+ <div class="timeline-entry-inner">
+ <div class="timeline-icon">
+ <user-avatar-link
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ />
+ </div>
+ <div class="timeline-content">
+ <div class="note-header">
+ <issue-note-header
+ :author="author"
+ :created-at="note.created_at"
+ :note-id="note.id"
+ action-text="commented"
+ />
+ <issue-note-actions
+ :author-id="author.id"
+ :note-id="note.id"
+ :access-level="note.human_access"
+ :can-edit="note.current_user.can_edit"
+ :can-delete="note.current_user.can_edit"
+ :can-report-as-abuse="canReportAsAbuse"
+ :report-abuse-path="note.report_abuse_path"
+ @handleEdit="editHandler"
+ @handleDelete="deleteHandler"
+ />
+ </div>
+ <issue-note-body
+ :note="note"
+ :can-edit="note.current_user.can_edit"
+ :is-editing="isEditing"
+ @handleFormUpdate="formUpdateHandler"
+ @cancelFormEdition="formCancelHandler"
+ ref="noteBody"
+ />
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue
new file mode 100644
index 00000000000..60c172321d1
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_note_actions.vue
@@ -0,0 +1,167 @@
+<script>
+ import { mapGetters } from 'vuex';
+ import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
+ import emojiSmile from 'icons/_emoji_smile.svg';
+ import emojiSmiley from 'icons/_emoji_smiley.svg';
+ import editSvg from 'icons/_icon_pencil.svg';
+ import ellipsisSvg from 'icons/_ellipsis_v.svg';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import tooltip from '../../vue_shared/directives/tooltip';
+
+ export default {
+ name: 'issueNoteActions',
+ props: {
+ authorId: {
+ type: Number,
+ required: true,
+ },
+ noteId: {
+ type: Number,
+ required: true,
+ },
+ accessLevel: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ reportAbusePath: {
+ type: String,
+ required: true,
+ },
+ canEdit: {
+ type: Boolean,
+ required: true,
+ },
+ canDelete: {
+ type: Boolean,
+ required: true,
+ },
+ canReportAsAbuse: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ directives: {
+ tooltip,
+ },
+ components: {
+ loadingIcon,
+ },
+ computed: {
+ ...mapGetters([
+ 'getUserDataByProp',
+ ]),
+ shouldShowActionsDropdown() {
+ return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
+ },
+ canAddAwardEmoji() {
+ return this.currentUserId;
+ },
+ isAuthoredByCurrentUser() {
+ return this.authorId === this.currentUserId;
+ },
+ currentUserId() {
+ return this.getUserDataByProp('id');
+ },
+ },
+ methods: {
+ onEdit() {
+ this.$emit('handleEdit');
+ },
+ onDelete() {
+ this.$emit('handleDelete');
+ },
+ },
+ created() {
+ this.emojiSmiling = emojiSmiling;
+ this.emojiSmile = emojiSmile;
+ this.emojiSmiley = emojiSmiley;
+ this.editSvg = editSvg;
+ this.ellipsisSvg = ellipsisSvg;
+ },
+ };
+</script>
+
+<template>
+ <div class="note-actions">
+ <span
+ v-if="accessLevel"
+ class="note-role">{{accessLevel}}</span>
+ <div
+ v-if="canAddAwardEmoji"
+ class="note-actions-item">
+ <a
+ v-tooltip
+ :class="{ 'js-user-authored': isAuthoredByCurrentUser }"
+ class="note-action-button note-emoji-button js-add-award js-note-emoji"
+ data-position="right"
+ data-placement="bottom"
+ data-container="body"
+ href="#"
+ title="Add reaction">
+ <loading-icon :inline="true" />
+ <span
+ v-html="emojiSmiling"
+ class="link-highlight award-control-icon-neutral">
+ </span>
+ <span
+ v-html="emojiSmiley"
+ class="link-highlight award-control-icon-positive">
+ </span>
+ <span
+ v-html="emojiSmile"
+ class="link-highlight award-control-icon-super-positive">
+ </span>
+ </a>
+ </div>
+ <div
+ v-if="canEdit"
+ class="note-actions-item">
+ <button
+ @click="onEdit"
+ v-tooltip
+ type="button"
+ title="Edit comment"
+ class="note-action-button js-note-edit btn btn-transparent"
+ data-container="body"
+ data-placement="bottom">
+ <span
+ v-html="editSvg"
+ class="link-highlight"></span>
+ </button>
+ </div>
+ <div
+ v-if="shouldShowActionsDropdown"
+ class="dropdown more-actions note-actions-item">
+ <button
+ v-tooltip
+ type="button"
+ title="More actions"
+ class="note-action-button more-actions-toggle btn btn-transparent"
+ data-toggle="dropdown"
+ data-container="body"
+ data-placement="bottom">
+ <span
+ class="icon"
+ v-html="ellipsisSvg"></span>
+ </button>
+ <ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
+ <li v-if="canReportAsAbuse">
+ <a :href="reportAbusePath">
+ Report as abuse
+ </a>
+ </li>
+ <li v-if="canEdit">
+ <button
+ @click.prevent="onDelete"
+ class="btn btn-transparent js-note-delete js-note-delete"
+ type="button">
+ <span class="text-danger">
+ Delete comment
+ </span>
+ </button>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_note_attachment.vue b/app/assets/javascripts/notes/components/issue_note_attachment.vue
new file mode 100644
index 00000000000..7134a3eb47e
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_note_attachment.vue
@@ -0,0 +1,37 @@
+<script>
+ export default {
+ name: 'issueNoteAttachment',
+ props: {
+ attachment: {
+ type: Object,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="note-attachment">
+ <a
+ v-if="attachment.image"
+ :href="attachment.url"
+ target="_blank"
+ rel="noopener noreferrer">
+ <img
+ :src="attachment.url"
+ class="note-image-attach" />
+ </a>
+ <div class="attachment">
+ <a
+ v-if="attachment.url"
+ :href="attachment.url"
+ target="_blank"
+ rel="noopener noreferrer">
+ <i
+ class="fa fa-paperclip"
+ aria-hidden="true"></i>
+ {{attachment.filename}}
+ </a>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_note_awards_list.vue b/app/assets/javascripts/notes/components/issue_note_awards_list.vue
new file mode 100644
index 00000000000..d42e61e3899
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_note_awards_list.vue
@@ -0,0 +1,228 @@
+<script>
+ /* global Flash */
+
+ import { mapActions, mapGetters } from 'vuex';
+ import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
+ import emojiSmile from 'icons/_emoji_smile.svg';
+ import emojiSmiley from 'icons/_emoji_smiley.svg';
+ import { glEmojiTag } from '../../emoji';
+ import tooltip from '../../vue_shared/directives/tooltip';
+
+ export default {
+ props: {
+ awards: {
+ type: Array,
+ required: true,
+ },
+ toggleAwardPath: {
+ type: String,
+ required: true,
+ },
+ noteAuthorId: {
+ type: Number,
+ required: true,
+ },
+ noteId: {
+ type: Number,
+ required: true,
+ },
+ },
+ directives: {
+ tooltip,
+ },
+ computed: {
+ ...mapGetters([
+ 'getUserData',
+ ]),
+ // `this.awards` is an array with emojis but they are not grouped by emoji name. See below.
+ // [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ]
+ // This method will group emojis by their name as an Object. See below.
+ // {
+ // foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ],
+ // bar: [ { name: bar, user: user1 } ]
+ // }
+ // We need to do this otherwise we will render the same emoji over and over again.
+ groupedAwards() {
+ const awards = this.awards.reduce((acc, award) => {
+ if (Object.prototype.hasOwnProperty.call(acc, award.name)) {
+ acc[award.name].push(award);
+ } else {
+ Object.assign(acc, { [award.name]: [award] });
+ }
+
+ return acc;
+ }, {});
+
+ const orderedAwards = {};
+ const { thumbsdown, thumbsup } = awards;
+ // Always show thumbsup and thumbsdown first
+ if (thumbsup) {
+ orderedAwards.thumbsup = thumbsup;
+ delete awards.thumbsup;
+ }
+ if (thumbsdown) {
+ orderedAwards.thumbsdown = thumbsdown;
+ delete awards.thumbsdown;
+ }
+
+ return Object.assign({}, orderedAwards, awards);
+ },
+ isAuthoredByMe() {
+ return this.noteAuthorId === this.getUserData.id;
+ },
+ isLoggedIn() {
+ return this.getUserData.id;
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'toggleAwardRequest',
+ ]),
+ getAwardHTML(name) {
+ return glEmojiTag(name);
+ },
+ getAwardClassBindings(awardList, awardName) {
+ return {
+ active: this.hasReactionByCurrentUser(awardList),
+ disabled: !this.canInteractWithEmoji(awardList, awardName),
+ };
+ },
+ canInteractWithEmoji(awardList, awardName) {
+ let isAllowed = true;
+ const restrictedEmojis = ['thumbsup', 'thumbsdown'];
+
+ // Users can not add :+1: and :-1: to their own notes
+ if (this.getUserData.id === this.noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) {
+ isAllowed = false;
+ }
+
+ return this.getUserData.id && isAllowed;
+ },
+ hasReactionByCurrentUser(awardList) {
+ return awardList.filter(award => award.user.id === this.getUserData.id).length;
+ },
+ awardTitle(awardsList) {
+ const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList);
+ const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10;
+ let awardList = awardsList;
+
+ // Filter myself from list if I am awarded.
+ if (hasReactionByCurrentUser) {
+ awardList = awardList.filter(award => award.user.id !== this.getUserData.id);
+ }
+
+ // Get only 9-10 usernames to show in tooltip text.
+ const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name);
+
+ // Get the remaining list to use in `and x more` text.
+ const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length);
+
+ // Add myself to the begining of the list so title will start with You.
+ if (hasReactionByCurrentUser) {
+ namesToShow.unshift('You');
+ }
+
+ let title = '';
+
+ // We have 10+ awarded user, join them with comma and add `and x more`.
+ if (remainingAwardList.length) {
+ title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`;
+ } else if (namesToShow.length > 1) {
+ // Join all names with comma but not the last one, it will be added with and text.
+ title = namesToShow.slice(0, namesToShow.length - 1).join(', ');
+ // If we have more than 2 users we need an extra comma before and text.
+ title += namesToShow.length > 2 ? ',' : '';
+ title += ` and ${namesToShow.slice(-1)}`; // Append and text
+ } else { // We have only 2 users so join them with and.
+ title = namesToShow.join(' and ');
+ }
+
+ return title;
+ },
+ handleAward(awardName) {
+ if (!this.isLoggedIn) {
+ return;
+ }
+
+ let parsedName;
+
+ // 100 and 1234 emoji are a number. Callback for v-for click sends it as a string
+ switch (awardName) {
+ case '100':
+ parsedName = 100;
+ break;
+ case '1234':
+ parsedName = 1234;
+ break;
+ default:
+ parsedName = awardName;
+ break;
+ }
+
+ const data = {
+ endpoint: this.toggleAwardPath,
+ noteId: this.noteId,
+ awardName: parsedName,
+ };
+
+ this.toggleAwardRequest(data)
+ .catch(() => Flash('Something went wrong on our end.'));
+ },
+ },
+ created() {
+ this.emojiSmiling = emojiSmiling;
+ this.emojiSmile = emojiSmile;
+ this.emojiSmiley = emojiSmiley;
+ },
+ };
+</script>
+
+<template>
+ <div class="note-awards">
+ <div class="awards js-awards-block">
+ <button
+ v-tooltip
+ v-for="(awardList, awardName, index) in groupedAwards"
+ :key="index"
+ :class="getAwardClassBindings(awardList, awardName)"
+ :title="awardTitle(awardList)"
+ @click="handleAward(awardName)"
+ class="btn award-control"
+ data-placement="bottom"
+ type="button">
+ <span v-html="getAwardHTML(awardName)"></span>
+ <span class="award-control-text js-counter">
+ {{awardList.length}}
+ </span>
+ </button>
+ <div
+ v-if="isLoggedIn"
+ class="award-menu-holder">
+ <button
+ v-tooltip
+ :class="{ 'js-user-authored': isAuthoredByMe }"
+ class="award-control btn js-add-award"
+ title="Add reaction"
+ aria-label="Add reaction"
+ data-placement="bottom"
+ type="button">
+ <span
+ v-html="emojiSmiling"
+ class="award-control-icon award-control-icon-neutral">
+ </span>
+ <span
+ v-html="emojiSmiley"
+ class="award-control-icon award-control-icon-positive">
+ </span>
+ <span
+ v-html="emojiSmile"
+ class="award-control-icon award-control-icon-super-positive">
+ </span>
+ <i
+ aria-hidden="true"
+ class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>
+ </button>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/issue_note_body.vue
new file mode 100644
index 00000000000..5f9003bfd87
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_note_body.vue
@@ -0,0 +1,122 @@
+<script>
+ import issueNoteEditedText from './issue_note_edited_text.vue';
+ import issueNoteAwardsList from './issue_note_awards_list.vue';
+ import issueNoteAttachment from './issue_note_attachment.vue';
+ import issueNoteForm from './issue_note_form.vue';
+ import TaskList from '../../task_list';
+ import autosave from '../mixins/autosave';
+
+ export default {
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ canEdit: {
+ type: Boolean,
+ required: true,
+ },
+ isEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ mixins: [
+ autosave,
+ ],
+ components: {
+ issueNoteEditedText,
+ issueNoteAwardsList,
+ issueNoteAttachment,
+ issueNoteForm,
+ },
+ computed: {
+ noteBody() {
+ return this.note.note;
+ },
+ },
+ methods: {
+ renderGFM() {
+ $(this.$refs['note-body']).renderGFM();
+ },
+ initTaskList() {
+ if (this.canEdit) {
+ this.taskList = new TaskList({
+ dataType: 'note',
+ fieldName: 'note',
+ selector: '.notes',
+ });
+ }
+ },
+ handleFormUpdate(note, parentElement, callback) {
+ this.$emit('handleFormUpdate', note, parentElement, callback);
+ },
+ formCancelHandler(shouldConfirm, isDirty) {
+ this.$emit('cancelFormEdition', shouldConfirm, isDirty);
+ },
+ },
+ mounted() {
+ this.renderGFM();
+ this.initTaskList();
+
+ if (this.isEditing) {
+ this.initAutoSave();
+ }
+ },
+ updated() {
+ this.initTaskList();
+ this.renderGFM();
+
+ if (this.isEditing) {
+ if (!this.autosave) {
+ this.initAutoSave();
+ } else {
+ this.setAutoSave();
+ }
+ }
+ },
+ };
+</script>
+
+<template>
+ <div
+ :class="{ 'js-task-list-container': canEdit }"
+ ref="note-body"
+ class="note-body">
+ <div
+ v-html="note.note_html"
+ class="note-text md"></div>
+ <issue-note-form
+ v-if="isEditing"
+ ref="noteForm"
+ @handleFormUpdate="handleFormUpdate"
+ @cancelFormEdition="formCancelHandler"
+ :is-editing="isEditing"
+ :note-body="noteBody"
+ :note-id="note.id"
+ />
+ <textarea
+ v-if="canEdit"
+ v-model="note.note"
+ :data-update-url="note.path"
+ class="hidden js-task-list-field"></textarea>
+ <issue-note-edited-text
+ v-if="note.last_edited_at"
+ :edited-at="note.last_edited_at"
+ :edited-by="note.last_edited_by"
+ action-text="Edited"
+ />
+ <issue-note-awards-list
+ v-if="note.award_emoji.length"
+ :note-id="note.id"
+ :note-author-id="note.author.id"
+ :awards="note.award_emoji"
+ :toggle-award-path="note.toggle_award_path"
+ />
+ <issue-note-attachment
+ v-if="note.attachment"
+ :attachment="note.attachment"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_note_edited_text.vue b/app/assets/javascripts/notes/components/issue_note_edited_text.vue
new file mode 100644
index 00000000000..49e09f0ecc5
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_note_edited_text.vue
@@ -0,0 +1,47 @@
+<script>
+ import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
+
+ export default {
+ name: 'editedNoteText',
+ props: {
+ actionText: {
+ type: String,
+ required: true,
+ },
+ editedAt: {
+ type: String,
+ required: true,
+ },
+ editedBy: {
+ type: Object,
+ required: false,
+ },
+ className: {
+ type: String,
+ required: false,
+ default: 'edited-text',
+ },
+ },
+ components: {
+ timeAgoTooltip,
+ },
+ };
+</script>
+
+<template>
+ <div :class="className">
+ {{actionText}}
+ <time-ago-tooltip
+ :time="editedAt"
+ tooltip-placement="bottom"
+ />
+ <template v-if="editedBy">
+ by
+ <a
+ :href="editedBy.path"
+ class="js-vue-author author_link">
+ {{editedBy.name}}
+ </a>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/issue_note_form.vue
new file mode 100644
index 00000000000..626c0f2ce18
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_note_form.vue
@@ -0,0 +1,166 @@
+<script>
+ import { mapGetters } from 'vuex';
+ import eventHub from '../event_hub';
+ import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue';
+ import markdownField from '../../vue_shared/components/markdown/field.vue';
+
+ export default {
+ name: 'issueNoteForm',
+ props: {
+ noteBody: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ noteId: {
+ type: Number,
+ required: false,
+ },
+ saveButtonTitle: {
+ type: String,
+ required: false,
+ default: 'Save comment',
+ },
+ discussion: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ isEditing: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ note: this.noteBody,
+ conflictWhileEditing: false,
+ isSubmitting: false,
+ };
+ },
+ components: {
+ confidentialIssue,
+ markdownField,
+ },
+ computed: {
+ ...mapGetters([
+ 'getDiscussionLastNote',
+ 'getIssueDataByProp',
+ 'getNotesDataByProp',
+ 'getUserDataByProp',
+ ]),
+ noteHash() {
+ return `#note_${this.noteId}`;
+ },
+ markdownPreviewPath() {
+ return this.getIssueDataByProp('preview_note_path');
+ },
+ markdownDocsPath() {
+ return this.getNotesDataByProp('markdownDocsPath');
+ },
+ quickActionsDocsPath() {
+ return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined;
+ },
+ currentUserId() {
+ return this.getUserDataByProp('id');
+ },
+ isDisabled() {
+ return !this.note.length || this.isSubmitting;
+ },
+ isConfidentialIssue() {
+ return this.getIssueDataByProp('confidential');
+ },
+ },
+ methods: {
+ handleUpdate() {
+ this.isSubmitting = true;
+
+ this.$emit('handleFormUpdate', this.note, this.$refs.editNoteForm, () => {
+ this.isSubmitting = false;
+ });
+ },
+ editMyLastNote() {
+ if (this.note === '') {
+ const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion);
+
+ if (lastNoteInDiscussion) {
+ eventHub.$emit('enterEditMode', {
+ noteId: lastNoteInDiscussion.id,
+ });
+ }
+ }
+ },
+ cancelHandler(shouldConfirm = false) {
+ // Sends information about confirm message and if the textarea has changed
+ this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note);
+ },
+ },
+ mounted() {
+ this.$refs.textarea.focus();
+ },
+ watch: {
+ noteBody() {
+ if (this.note === this.noteBody) {
+ this.note = this.noteBody;
+ } else {
+ this.conflictWhileEditing = true;
+ }
+ },
+ },
+ };
+</script>
+
+<template>
+ <div ref="editNoteForm" class="note-edit-form current-note-edit-form">
+ <div
+ v-if="conflictWhileEditing"
+ class="js-conflict-edit-warning alert alert-danger">
+ This comment has changed since you started editing, please review the
+ <a
+ :href="noteHash"
+ target="_blank"
+ rel="noopener noreferrer">updated comment</a>
+ to ensure information is not lost.
+ </div>
+ <div class="flash-container timeline-content"></div>
+ <form
+ class="edit-note common-note-form js-quick-submit gfm-form">
+ <confidentialIssue v-if="isConfidentialIssue" />
+ <markdown-field
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :add-spacing-classes="false">
+ <textarea
+ id="note_note"
+ name="note[note]"
+ class="note-textarea js-gfm-input js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
+ :data-supports-quick-actions="!isEditing"
+ aria-label="Description"
+ v-model="note"
+ ref="textarea"
+ slot="textarea"
+ placeholder="Write a comment or drag your files here..."
+ @keydown.meta.enter="handleUpdate()"
+ @keydown.up="editMyLastNote()"
+ @keydown.esc="cancelHandler(true)">
+ </textarea>
+ </markdown-field>
+ <div class="note-form-actions clearfix">
+ <button
+ type="button"
+ @click="handleUpdate()"
+ :disabled="isDisabled"
+ class="js-vue-issue-save btn btn-save">
+ {{saveButtonTitle}}
+ </button>
+ <button
+ @click="cancelHandler()"
+ class="btn btn-cancel note-edit-cancel"
+ type="button">
+ Cancel
+ </button>
+ </div>
+ </form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_note_header.vue b/app/assets/javascripts/notes/components/issue_note_header.vue
new file mode 100644
index 00000000000..63aa3d777d0
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_note_header.vue
@@ -0,0 +1,118 @@
+<script>
+ import { mapActions } from 'vuex';
+ import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
+
+ export default {
+ props: {
+ author: {
+ type: Object,
+ required: true,
+ },
+ createdAt: {
+ type: String,
+ required: true,
+ },
+ actionText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ actionTextHtml: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ noteId: {
+ type: Number,
+ required: true,
+ },
+ includeToggle: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isExpanded: true,
+ };
+ },
+ components: {
+ timeAgoTooltip,
+ },
+ computed: {
+ toggleChevronClass() {
+ return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down';
+ },
+ noteTimestampLink() {
+ return `#note_${this.noteId}`;
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'setTargetNoteHash',
+ ]),
+ handleToggle() {
+ this.isExpanded = !this.isExpanded;
+ this.$emit('toggleHandler');
+ },
+ updateTargetNoteHash() {
+ this.setTargetNoteHash(this.noteTimestampLink);
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="note-header-info">
+ <a :href="author.path">
+ <span class="note-header-author-name">
+ {{author.name}}
+ </span>
+ <span class="note-headline-light">
+ @{{author.username}}
+ </span>
+ </a>
+ <span class="note-headline-light">
+ <span class="note-headline-meta">
+ <template v-if="actionText">
+ {{actionText}}
+ </template>
+ <span
+ v-if="actionTextHtml"
+ v-html="actionTextHtml"
+ class="system-note-message">
+ </span>
+ <a
+ :href="noteTimestampLink"
+ @click="updateTargetNoteHash"
+ class="note-timestamp">
+ <time-ago-tooltip
+ :time="createdAt"
+ tooltip-placement="bottom"
+ />
+ </a>
+ <i
+ class="fa fa-spinner fa-spin editing-spinner"
+ aria-label="Comment is being updated"
+ aria-hidden="true">
+ </i>
+ </span>
+ </span>
+ <div
+ v-if="includeToggle"
+ class="discussion-actions">
+ <button
+ @click="handleToggle"
+ class="note-action-button discussion-toggle-button js-vue-toggle-button"
+ type="button">
+ <i
+ :class="toggleChevronClass"
+ class="fa"
+ aria-hidden="true">
+ </i>
+ Toggle discussion
+ </button>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_note_icons.js b/app/assets/javascripts/notes/components/issue_note_icons.js
new file mode 100644
index 00000000000..d8e3cb4bc01
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_note_icons.js
@@ -0,0 +1,37 @@
+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_note_signed_out_widget.vue b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue
new file mode 100644
index 00000000000..77af3594c1c
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue
@@ -0,0 +1,28 @@
+<script>
+ import { mapGetters } from 'vuex';
+
+ export default {
+ name: 'singInLinksNotes',
+ computed: {
+ ...mapGetters([
+ 'getNotesDataByProp',
+ ]),
+ registerLink() {
+ return this.getNotesDataByProp('registerPath');
+ },
+ signInLink() {
+ return this.getNotesDataByProp('newSessionPath');
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="disabled-comment text-center">
+ Please
+ <a :href="registerLink">register</a>
+ or
+ <a :href="signInLink">sign in</a>
+ to reply
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue
new file mode 100644
index 00000000000..b6fc5e5036f
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_notes_app.vue
@@ -0,0 +1,151 @@
+<script>
+ /* global Flash */
+ import { mapGetters, mapActions } from 'vuex';
+ import store from '../stores/';
+ import * as constants from '../constants';
+ import issueNote from './issue_note.vue';
+ import issueDiscussion from './issue_discussion.vue';
+ import issueSystemNote from './issue_system_note.vue';
+ import issueCommentForm from './issue_comment_form.vue';
+ import placeholderNote from './issue_placeholder_note.vue';
+ import placeholderSystemNote from './issue_placeholder_system_note.vue';
+ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+ export default {
+ name: 'issueNotesApp',
+ props: {
+ issueData: {
+ type: Object,
+ required: true,
+ },
+ notesData: {
+ type: Object,
+ required: true,
+ },
+ userData: {
+ type: Object,
+ required: false,
+ default: {},
+ },
+ },
+ store,
+ data() {
+ return {
+ isLoading: true,
+ };
+ },
+ components: {
+ issueNote,
+ issueDiscussion,
+ issueSystemNote,
+ issueCommentForm,
+ loadingIcon,
+ placeholderNote,
+ placeholderSystemNote,
+ },
+ computed: {
+ ...mapGetters([
+ 'notes',
+ 'getNotesDataByProp',
+ ]),
+ },
+ methods: {
+ ...mapActions({
+ actionFetchNotes: 'fetchNotes',
+ poll: 'poll',
+ actionToggleAward: 'toggleAward',
+ scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
+ setNotesData: 'setNotesData',
+ setIssueData: 'setIssueData',
+ setUserData: 'setUserData',
+ setLastFetchedAt: 'setLastFetchedAt',
+ setTargetNoteHash: 'setTargetNoteHash',
+ }),
+ getComponentName(note) {
+ if (note.isPlaceholderNote) {
+ if (note.placeholderType === constants.SYSTEM_NOTE) {
+ return placeholderSystemNote;
+ }
+ return placeholderNote;
+ } else if (note.individual_note) {
+ return note.notes[0].system ? issueSystemNote : issueNote;
+ }
+
+ return issueDiscussion;
+ },
+ getComponentData(note) {
+ return note.individual_note ? note.notes[0] : note;
+ },
+ fetchNotes() {
+ return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath'))
+ .then(() => this.initPolling())
+ .then(() => {
+ this.isLoading = false;
+ })
+ .then(() => this.$nextTick())
+ .then(() => this.checkLocationHash())
+ .catch(() => {
+ this.isLoading = false;
+ Flash('Something went wrong while fetching issue comments. Please try again.');
+ });
+ },
+ initPolling() {
+ this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt'));
+
+ this.poll();
+ },
+ checkLocationHash() {
+ const hash = gl.utils.getLocationHash();
+ const element = document.getElementById(hash);
+
+ if (hash && element) {
+ this.setTargetNoteHash(hash);
+ this.scrollToNoteIfNeeded($(element));
+ }
+ },
+ },
+ created() {
+ this.setNotesData(this.notesData);
+ this.setIssueData(this.issueData);
+ this.setUserData(this.userData);
+ },
+ mounted() {
+ this.fetchNotes();
+
+ const parentElement = this.$el.parentElement;
+
+ if (parentElement &&
+ parentElement.classList.contains('js-vue-notes-event')) {
+ parentElement.addEventListener('toggleAward', (event) => {
+ const { awardName, noteId } = event.detail;
+ this.actionToggleAward({ awardName, noteId });
+ });
+ }
+ },
+ };
+</script>
+
+<template>
+ <div id="notes">
+ <div
+ v-if="isLoading"
+ class="js-loading loading">
+ <loading-icon />
+ </div>
+
+ <ul
+ v-if="!isLoading"
+ id="notes-list"
+ class="notes main-notes-list timeline">
+
+ <component
+ v-for="note in notes"
+ :is="getComponentName(note)"
+ :note="getComponentData(note)"
+ :key="note.id"
+ />
+ </ul>
+
+ <issue-comment-form />
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_placeholder_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_note.vue
new file mode 100644
index 00000000000..6921d91372f
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_placeholder_note.vue
@@ -0,0 +1,53 @@
+<script>
+ import { mapGetters } from 'vuex';
+ import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+
+ export default {
+ name: 'issuePlaceholderNote',
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ userAvatarLink,
+ },
+ computed: {
+ ...mapGetters([
+ 'getUserData',
+ ]),
+ },
+ };
+</script>
+
+<template>
+ <li class="note being-posted fade-in-half timeline-entry">
+ <div class="timeline-entry-inner">
+ <div class="timeline-icon">
+ <user-avatar-link
+ :link-href="getUserData.path"
+ :img-src="getUserData.avatar_url"
+ :img-size="40"
+ />
+ </div>
+ <div
+ :class="{ discussion: !note.individual_note }"
+ class="timeline-content">
+ <div class="note-header">
+ <div class="note-header-info">
+ <a :href="getUserData.path">
+ <span class="hidden-xs">{{getUserData.name}}</span>
+ <span class="note-headline-light">@{{getUserData.username}}</span>
+ </a>
+ </div>
+ </div>
+ <div class="note-body">
+ <div class="note-text">
+ <p>{{note.body}}</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue
new file mode 100644
index 00000000000..80a8ef56a83
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_placeholder_system_note.vue
@@ -0,0 +1,21 @@
+<script>
+ export default {
+ name: 'placeholderSystemNote',
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <li class="note system-note timeline-entry being-posted fade-in-half">
+ <div class="timeline-entry-inner">
+ <div class="timeline-content">
+ <em>{{note.body}}</em>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/notes/components/issue_system_note.vue b/app/assets/javascripts/notes/components/issue_system_note.vue
new file mode 100644
index 00000000000..5bb8f871b9d
--- /dev/null
+++ b/app/assets/javascripts/notes/components/issue_system_note.vue
@@ -0,0 +1,55 @@
+<script>
+ import { mapGetters } from 'vuex';
+ import iconsMap from './issue_note_icons';
+ import issueNoteHeader from './issue_note_header.vue';
+
+ export default {
+ name: 'systemNote',
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ issueNoteHeader,
+ },
+ computed: {
+ ...mapGetters([
+ 'targetNoteHash',
+ ]),
+ noteAnchorId() {
+ return `note_${this.note.id}`;
+ },
+ isTargetNote() {
+ return this.targetNoteHash === this.noteAnchorId;
+ },
+ },
+ created() {
+ this.svg = iconsMap[this.note.system_note_icon_name];
+ },
+ };
+</script>
+
+<template>
+ <li
+ :id="noteAnchorId"
+ :class="{ target: isTargetNote }"
+ class="note system-note timeline-entry">
+ <div class="timeline-entry-inner">
+ <div
+ class="timeline-icon"
+ v-html="svg">
+ </div>
+ <div class="timeline-content">
+ <div class="note-header">
+ <issue-note-header
+ :author="note.author"
+ :created-at="note.created_at"
+ :note-id="note.id"
+ :action-text-html="note.note_html" />
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
new file mode 100644
index 00000000000..a6961063c01
--- /dev/null
+++ b/app/assets/javascripts/notes/constants.js
@@ -0,0 +1,11 @@
+export const DISCUSSION_NOTE = 'DiscussionNote';
+export const DISCUSSION = 'discussion';
+export const NOTE = 'note';
+export const SYSTEM_NOTE = 'systemNote';
+export const COMMENT = 'comment';
+export const OPENED = 'opened';
+export const REOPENED = 'reopened';
+export const CLOSED = 'closed';
+export const EMOJI_THUMBSUP = 'thumbsup';
+export const EMOJI_THUMBSDOWN = 'thumbsdown';
+export const NOTEABLE_TYPE = 'Issue';
diff --git a/app/assets/javascripts/notes/event_hub.js b/app/assets/javascripts/notes/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/notes/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
new file mode 100644
index 00000000000..e2ea37408cf
--- /dev/null
+++ b/app/assets/javascripts/notes/index.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import issueNotesApp from './components/issue_notes_app.vue';
+
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#js-vue-notes',
+ components: {
+ issueNotesApp,
+ },
+ data() {
+ const notesDataset = document.getElementById('js-vue-notes').dataset;
+
+ return {
+ issueData: JSON.parse(notesDataset.issueData),
+ currentUserData: JSON.parse(notesDataset.currentUserData),
+ notesData: {
+ lastFetchedAt: notesDataset.lastFetchedAt,
+ discussionsPath: notesDataset.discussionsPath,
+ newSessionPath: notesDataset.newSessionPath,
+ registerPath: notesDataset.registerPath,
+ notesPath: notesDataset.notesPath,
+ markdownDocsPath: notesDataset.markdownDocsPath,
+ quickActionsDocsPath: notesDataset.quickActionsDocsPath,
+ },
+ };
+ },
+ render(createElement) {
+ return createElement('issue-notes-app', {
+ props: {
+ issueData: this.issueData,
+ notesData: this.notesData,
+ userData: this.currentUserData,
+ },
+ });
+ },
+}));
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
new file mode 100644
index 00000000000..5843b97f225
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -0,0 +1,16 @@
+/* globals Autosave */
+import '../../autosave';
+
+export default {
+ methods: {
+ initAutoSave() {
+ this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id], 'issue');
+ },
+ resetAutoSave() {
+ this.autosave.reset();
+ },
+ setAutoSave() {
+ this.autosave.save();
+ },
+ },
+};
diff --git a/app/assets/javascripts/notes/services/issue_notes_service.js b/app/assets/javascripts/notes/services/issue_notes_service.js
new file mode 100644
index 00000000000..b51b0cb2013
--- /dev/null
+++ b/app/assets/javascripts/notes/services/issue_notes_service.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default {
+ fetchNotes(endpoint) {
+ return Vue.http.get(endpoint);
+ },
+ deleteNote(endpoint) {
+ return Vue.http.delete(endpoint);
+ },
+ replyToDiscussion(endpoint, data) {
+ return Vue.http.post(endpoint, data, { emulateJSON: true });
+ },
+ updateNote(endpoint, data) {
+ return Vue.http.put(endpoint, data, { emulateJSON: true });
+ },
+ createNewNote(endpoint, data) {
+ return Vue.http.post(endpoint, data, { emulateJSON: true });
+ },
+ poll(data = {}) {
+ const { endpoint, lastFetchedAt } = data;
+ const options = {
+ headers: {
+ 'X-Last-Fetched-At': lastFetchedAt,
+ },
+ };
+
+ return Vue.http.get(endpoint, options);
+ },
+ toggleAward(endpoint, data) {
+ return Vue.http.post(endpoint, data, { emulateJSON: true });
+ },
+};
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
new file mode 100644
index 00000000000..13cd74bfa1c
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -0,0 +1,217 @@
+/* global Flash */
+import Visibility from 'visibilityjs';
+import Poll from '../../lib/utils/poll';
+import * as types from './mutation_types';
+import * as utils from './utils';
+import * as constants from '../constants';
+import service from '../services/issue_notes_service';
+import loadAwardsHandler from '../../awards_handler';
+import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
+
+let eTagPoll;
+
+export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
+export const setIssueData = ({ commit }, data) => commit(types.SET_ISSUE_DATA, data);
+export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
+export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
+export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
+export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
+export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
+
+export const fetchNotes = ({ commit }, path) => service
+ .fetchNotes(path)
+ .then(res => res.json())
+ .then((res) => {
+ commit(types.SET_INITIAL_NOTES, res);
+ });
+
+export const deleteNote = ({ commit }, note) => service
+ .deleteNote(note.path)
+ .then(() => {
+ commit(types.DELETE_NOTE, note);
+ });
+
+export const updateNote = ({ commit }, { endpoint, note }) => service
+ .updateNote(endpoint, note)
+ .then(res => res.json())
+ .then((res) => {
+ commit(types.UPDATE_NOTE, res);
+ });
+
+export const replyToDiscussion = ({ commit }, { endpoint, data }) => service
+ .replyToDiscussion(endpoint, data)
+ .then(res => res.json())
+ .then((res) => {
+ commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
+
+ return res;
+ });
+
+export const createNewNote = ({ commit }, { endpoint, data }) => service
+ .createNewNote(endpoint, data)
+ .then(res => res.json())
+ .then((res) => {
+ if (!res.errors) {
+ commit(types.ADD_NEW_NOTE, res);
+ }
+ return res;
+ });
+
+export const removePlaceholderNotes = ({ commit }) =>
+ commit(types.REMOVE_PLACEHOLDER_NOTES);
+
+export const saveNote = ({ commit, dispatch }, noteData) => {
+ const { note } = noteData.data.note;
+ let placeholderText = note;
+ const hasQuickActions = utils.hasQuickActions(placeholderText);
+ const replyId = noteData.data.in_reply_to_discussion_id;
+ const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote';
+
+ commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
+ $('.notes-form .flash-container').hide(); // hide previous flash notification
+
+ if (hasQuickActions) {
+ placeholderText = utils.stripQuickActions(placeholderText);
+ }
+
+ if (placeholderText.length) {
+ commit(types.SHOW_PLACEHOLDER_NOTE, {
+ noteBody: placeholderText,
+ replyId,
+ });
+ }
+
+ if (hasQuickActions) {
+ commit(types.SHOW_PLACEHOLDER_NOTE, {
+ isSystemNote: true,
+ noteBody: utils.getQuickActionText(note),
+ replyId,
+ });
+ }
+
+ return dispatch(methodToDispatch, noteData)
+ .then((res) => {
+ const { errors } = res;
+ const commandsChanges = res.commands_changes;
+
+ if (hasQuickActions && errors && Object.keys(errors).length) {
+ eTagPoll.makeRequest();
+
+ $('.js-gfm-input').trigger('clear-commands-cache.atwho');
+ Flash('Commands applied', 'notice', $(noteData.flashContainer));
+ }
+
+ if (commandsChanges) {
+ if (commandsChanges.emoji_award) {
+ const votesBlock = $('.js-awards-block').eq(0);
+
+ loadAwardsHandler()
+ .then((awardsHandler) => {
+ awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award);
+ awardsHandler.scrollToAwards();
+ })
+ .catch(() => {
+ Flash(
+ 'Something went wrong while adding your award. Please try again.',
+ null,
+ $(noteData.flashContainer),
+ );
+ });
+ }
+
+ if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) {
+ sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
+ }
+ }
+
+ if (errors && errors.commands_only) {
+ Flash(errors.commands_only, 'notice', $(noteData.flashContainer));
+ }
+ commit(types.REMOVE_PLACEHOLDER_NOTES);
+
+ return res;
+ });
+};
+
+const pollSuccessCallBack = (resp, commit, state, getters) => {
+ if (resp.notes && resp.notes.length) {
+ const { notesById } = getters;
+
+ resp.notes.forEach((note) => {
+ if (notesById[note.id]) {
+ commit(types.UPDATE_NOTE, note);
+ } else if (note.type === constants.DISCUSSION_NOTE) {
+ const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
+
+ if (discussion) {
+ commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
+ } else {
+ commit(types.ADD_NEW_NOTE, note);
+ }
+ } else {
+ commit(types.ADD_NEW_NOTE, note);
+ }
+ });
+ }
+
+ commit(types.SET_LAST_FETCHED_AT, resp.lastFetchedAt);
+
+ return resp;
+};
+
+export const poll = ({ commit, state, getters }) => {
+ const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt };
+
+ eTagPoll = new Poll({
+ resource: service,
+ method: 'poll',
+ data: requestData,
+ successCallback: resp => resp.json()
+ .then(data => pollSuccessCallBack(data, commit, state, getters)),
+ errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
+ });
+
+ if (!Visibility.hidden()) {
+ eTagPoll.makeRequest();
+ } else {
+ service.poll(requestData);
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ eTagPoll.restart();
+ } else {
+ eTagPoll.stop();
+ }
+ });
+};
+
+export const fetchData = ({ commit, state, getters }) => {
+ const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt };
+
+ service.poll(requestData)
+ .then(resp => resp.json)
+ .then(data => pollSuccessCallBack(data, commit, state, getters))
+ .catch(() => Flash('Something went wrong while fetching latest comments.'));
+};
+
+export const toggleAward = ({ commit, state, getters, dispatch }, { awardName, noteId }) => {
+ commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] });
+};
+
+export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => {
+ const { endpoint, awardName } = data;
+
+ return service
+ .toggleAward(endpoint, { name: awardName })
+ .then(res => res.json())
+ .then(() => {
+ dispatch('toggleAward', data);
+ });
+};
+
+export const scrollToNoteIfNeeded = (context, el) => {
+ if (!gl.utils.isInViewport(el[0])) {
+ gl.utils.scrollToElement(el);
+ }
+};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
new file mode 100644
index 00000000000..1f0c6af6156
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -0,0 +1,31 @@
+import _ from 'underscore';
+
+export const notes = state => state.notes;
+export const targetNoteHash = state => state.targetNoteHash;
+
+export const getNotesData = state => state.notesData;
+export const getNotesDataByProp = state => prop => state.notesData[prop];
+
+export const getIssueData = state => state.issueData;
+export const getIssueDataByProp = state => prop => state.issueData[prop];
+
+export const getUserData = state => state.userData || {};
+export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
+
+export const notesById = state => state.notes.reduce((acc, note) => {
+ note.notes.every(n => Object.assign(acc, { [n.id]: n }));
+ return acc;
+}, {});
+
+const reverseNotes = array => array.slice(0).reverse();
+const isLastNote = (note, state) => !note.system &&
+ state.userData && note.author &&
+ note.author.id === state.userData.id;
+
+export const getCurrentUserLastNote = state => _.flatten(
+ reverseNotes(state.notes)
+ .map(note => reverseNotes(note.notes)),
+ ).find(el => isLastNote(el, state));
+
+export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes)
+ .find(el => isLastNote(el, state));
diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js
new file mode 100644
index 00000000000..8e0c8531bbc
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/index.js
@@ -0,0 +1,23 @@
+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: {
+ notes: [],
+ targetNoteHash: null,
+ lastFetchedAt: null,
+
+ // holds endpoints and permissions provided through haml
+ notesData: {},
+ userData: {},
+ issueData: {},
+ },
+ actions,
+ getters,
+ mutations,
+});
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
new file mode 100644
index 00000000000..cd71533ba9d
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -0,0 +1,14 @@
+export const ADD_NEW_NOTE = 'ADD_NEW_NOTE';
+export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
+export const DELETE_NOTE = 'DELETE_NOTE';
+export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
+export const SET_NOTES_DATA = 'SET_NOTES_DATA';
+export const SET_ISSUE_DATA = 'SET_ISSUE_DATA';
+export const SET_USER_DATA = 'SET_USER_DATA';
+export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES';
+export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
+export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH';
+export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
+export const TOGGLE_AWARD = 'TOGGLE_AWARD';
+export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
+export const UPDATE_NOTE = 'UPDATE_NOTE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
new file mode 100644
index 00000000000..3b2b2089d6e
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -0,0 +1,151 @@
+import * as utils from './utils';
+import * as types from './mutation_types';
+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);
+ },
+
+ [types.ADD_NEW_REPLY_TO_DISCUSSION](state, note) {
+ const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+
+ if (noteObj) {
+ noteObj.notes.push(note);
+ }
+ },
+
+ [types.DELETE_NOTE](state, note) {
+ const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+
+ if (noteObj.individual_note) {
+ state.notes.splice(state.notes.indexOf(noteObj), 1);
+ } else {
+ const comment = utils.findNoteObjectById(noteObj.notes, note.id);
+ noteObj.notes.splice(noteObj.notes.indexOf(comment), 1);
+
+ if (!noteObj.notes.length) {
+ state.notes.splice(state.notes.indexOf(noteObj), 1);
+ }
+ }
+ },
+
+ [types.REMOVE_PLACEHOLDER_NOTES](state) {
+ const { notes } = state;
+
+ for (let i = notes.length - 1; i >= 0; i -= 1) {
+ const note = notes[i];
+ const children = note.notes;
+
+ if (children.length && !note.individual_note) { // remove placeholder from discussions
+ for (let j = children.length - 1; j >= 0; j -= 1) {
+ if (children[j].isPlaceholderNote) {
+ children.splice(j, 1);
+ }
+ }
+ } else if (note.isPlaceholderNote) { // remove placeholders from state root
+ notes.splice(i, 1);
+ }
+ }
+ },
+
+ [types.SET_NOTES_DATA](state, data) {
+ Object.assign(state, { notesData: data });
+ },
+
+ [types.SET_ISSUE_DATA](state, data) {
+ Object.assign(state, { issueData: data });
+ },
+
+ [types.SET_USER_DATA](state, data) {
+ Object.assign(state, { userData: data });
+ },
+ [types.SET_INITIAL_NOTES](state, notesData) {
+ const notes = [];
+
+ notesData.forEach((note) => {
+ // To support legacy notes, should be very rare case.
+ if (note.individual_note && note.notes.length > 1) {
+ note.notes.forEach((n) => {
+ const nn = Object.assign({}, note);
+ nn.notes = [n]; // override notes array to only have one item to mimick individual_note
+ notes.push(nn);
+ });
+ } else {
+ notes.push(note);
+ }
+ });
+
+ Object.assign(state, { notes });
+ },
+
+ [types.SET_LAST_FETCHED_AT](state, fetchedAt) {
+ Object.assign(state, { lastFetchedAt: fetchedAt });
+ },
+
+ [types.SET_TARGET_NOTE_HASH](state, hash) {
+ Object.assign(state, { targetNoteHash: hash });
+ },
+
+ [types.SHOW_PLACEHOLDER_NOTE](state, data) {
+ let notesArr = state.notes;
+ if (data.replyId) {
+ notesArr = utils.findNoteObjectById(notesArr, data.replyId).notes;
+ }
+
+ notesArr.push({
+ individual_note: true,
+ isPlaceholderNote: true,
+ placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE,
+ notes: [
+ {
+ body: data.noteBody,
+ },
+ ],
+ });
+ },
+
+ [types.TOGGLE_AWARD](state, data) {
+ const { awardName, note } = data;
+ const { id, name, username } = state.userData;
+
+ const hasEmojiAwardedByCurrentUser = note.award_emoji
+ .filter(emoji => emoji.name === data.awardName && emoji.user.id === id);
+
+ if (hasEmojiAwardedByCurrentUser.length) {
+ // If current user has awarded this emoji, remove it.
+ note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 1);
+ } else {
+ note.award_emoji.push({
+ name: awardName,
+ user: { id, name, username },
+ });
+ }
+ },
+
+ [types.TOGGLE_DISCUSSION](state, { discussionId }) {
+ const discussion = utils.findNoteObjectById(state.notes, discussionId);
+
+ discussion.expanded = !discussion.expanded;
+ },
+
+ [types.UPDATE_NOTE](state, note) {
+ const noteObj = utils.findNoteObjectById(state.notes, note.discussion_id);
+
+ if (noteObj.individual_note) {
+ noteObj.notes.splice(0, 1, note);
+ } else {
+ const comment = utils.findNoteObjectById(noteObj.notes, note.id);
+ noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
+ }
+ },
+};
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
new file mode 100644
index 00000000000..6074115e855
--- /dev/null
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -0,0 +1,31 @@
+import AjaxCache from '~/lib/utils/ajax_cache';
+
+const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
+
+export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
+
+export const getQuickActionText = (note) => {
+ let text = 'Applying command';
+ const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
+
+ const executedCommands = quickActions.filter((command) => {
+ const commandRegex = new RegExp(`/${command.name}`);
+ return commandRegex.test(note);
+ });
+
+ if (executedCommands && executedCommands.length) {
+ if (executedCommands.length > 1) {
+ text = 'Applying multiple commands';
+ } else {
+ const commandDescription = executedCommands[0].description.toLowerCase();
+ text = `Applying command to ${commandDescription}`;
+ }
+ }
+
+ return text;
+};
+
+export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
+
+export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
+
diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js
deleted file mode 100644
index c827b7402dc..00000000000
--- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import Vue from 'vue';
-import Cookies from 'js-cookie';
-import Translate from '../../vue_shared/translate';
-import illustrationSvg from '../icons/intro_illustration.svg';
-
-Vue.use(Translate);
-
-const cookieKey = 'pipeline_schedules_callout_dismissed';
-
-export default {
- name: 'PipelineSchedulesCallout',
- data() {
- return {
- docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
- illustrationSvg,
- calloutDismissed: Cookies.get(cookieKey) === 'true',
- };
- },
- methods: {
- dismissCallout() {
- this.calloutDismissed = true;
- Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
- },
- },
- template: `
- <div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
- <div class="bordered-box landing content-block">
- <button
- id="dismiss-callout-btn"
- class="btn btn-default close"
- @click="dismissCallout">
- <i class="fa fa-times"></i>
- </button>
- <div class="svg-container" v-html="illustrationSvg"></div>
- <div class="user-callout-copy">
- <h4>{{ __('Scheduling Pipelines') }}</h4>
- <p>
- {{ __('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.') }}
- </p>
- <p> {{ __('Learn more in the') }}
- <a
- :href="docsUrl"
- target="_blank"
- rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
- </p>
- </div>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue
new file mode 100644
index 00000000000..6e0bc2d697a
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue
@@ -0,0 +1,59 @@
+<script>
+ import Vue from 'vue';
+ import Cookies from 'js-cookie';
+ import Translate from '../../vue_shared/translate';
+ import illustrationSvg from '../icons/intro_illustration.svg';
+
+ Vue.use(Translate);
+
+ const cookieKey = 'pipeline_schedules_callout_dismissed';
+
+ export default {
+ name: 'PipelineSchedulesCallout',
+ data() {
+ return {
+ docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
+ calloutDismissed: Cookies.get(cookieKey) === 'true',
+ };
+ },
+ methods: {
+ dismissCallout() {
+ this.calloutDismissed = true;
+ Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 });
+ },
+ },
+ created() {
+ this.illustrationSvg = illustrationSvg;
+ },
+ };
+</script>
+<template>
+ <div
+ v-if="!calloutDismissed"
+ class="pipeline-schedules-user-callout user-callout">
+ <div class="bordered-box landing content-block">
+ <button
+ id="dismiss-callout-btn"
+ class="btn btn-default close"
+ @click="dismissCallout">
+ <i
+ aria-hidden="true"
+ class="fa fa-times">
+ </i>
+ </button>
+ <div class="svg-container" v-html="illustrationSvg"></div>
+ <div class="user-callout-copy">
+ <h4>{{ __('Scheduling Pipelines') }}</h4>
+ <p>
+ {{ __('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.') }}
+ </p>
+ <p> {{ __('Learn more in the') }}
+ <a
+ :href="docsUrl"
+ target="_blank"
+ rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period -->
+ </p>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js
index 6584549ad06..a6c945e22b0 100644
--- a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js
+++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import PipelineSchedulesCallout from './components/pipeline_schedules_callout';
+import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#pipeline-schedules-callout',
diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index 2944689a5a7..3e5d6d15909 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -48,10 +48,31 @@
return `${this.job.name} - ${this.job.status.label}`;
},
},
+
+ methods: {
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item'))
+ .on('click', (e) => {
+ e.stopPropagation();
+ });
+ },
+ },
+
+ mounted() {
+ this.stopDropdownClickPropagation();
+ },
};
</script>
<template>
- <div>
+ <div class="ci-job-dropdown-container">
<button
v-tooltip
type="button"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue
index 1f5ed3f1074..3933509a6f4 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue
@@ -75,7 +75,7 @@
};
</script>
<template>
- <div>
+ <div class="ci-job-component">
<a
v-tooltip
v-if="job.status.details_path"
diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
index d8856e10668..f46d21bd6d7 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
@@ -26,7 +26,7 @@
};
</script>
<template>
- <span>
+ <span class="ci-job-name-component">
<ci-icon
:status="status" />
diff --git a/app/assets/javascripts/pipelines/components/navigation_tabs.vue b/app/assets/javascripts/pipelines/components/navigation_tabs.vue
index d2f6d47f043..73f7e3a0cad 100644
--- a/app/assets/javascripts/pipelines/components/navigation_tabs.vue
+++ b/app/assets/javascripts/pipelines/components/navigation_tabs.vue
@@ -1,23 +1,29 @@
<script>
-export default {
- name: 'PipelineNavigationTabs',
- props: {
- scope: {
- type: String,
- required: true,
+ export default {
+ name: 'PipelineNavigationTabs',
+ props: {
+ scope: {
+ type: String,
+ required: true,
+ },
+ count: {
+ type: Object,
+ required: true,
+ },
+ paths: {
+ type: Object,
+ required: true,
+ },
},
- count: {
- type: Object,
- required: true,
+ mounted() {
+ $(document).trigger('init.scrolling-tabs');
},
- paths: {
- type: Object,
- required: true,
+ methods: {
+ shouldRenderBadge(count) {
+ // 0 is valid in a badge, but evaluates to false, we need to check for undefined
+ return count !== undefined;
+ },
},
- },
- mounted() {
- $(document).trigger('init.scrolling-tabs');
- },
};
</script>
<template>
@@ -27,7 +33,9 @@ export default {
:class="{ active: scope === 'all'}">
<a :href="paths.allPath">
All
- <span class="badge js-totalbuilds-count">
+ <span
+ v-if="shouldRenderBadge(count.all)"
+ class="badge js-totalbuilds-count">
{{count.all}}
</span>
</a>
@@ -37,7 +45,9 @@ export default {
:class="{ active: scope === 'pending'}">
<a :href="paths.pendingPath">
Pending
- <span class="badge">
+ <span
+ v-if="shouldRenderBadge(count.pending)"
+ class="badge">
{{count.pending}}
</span>
</a>
@@ -47,7 +57,9 @@ export default {
:class="{ active: scope === 'running'}">
<a :href="paths.runningPath">
Running
- <span class="badge">
+ <span
+ v-if="shouldRenderBadge(count.running)"
+ class="badge">
{{count.running}}
</span>
</a>
@@ -57,7 +69,9 @@ export default {
:class="{ active: scope === 'finished'}">
<a :href="paths.finishedPath">
Finished
- <span class="badge">
+ <span
+ v-if="shouldRenderBadge(count.finished)"
+ class="badge">
{{count.finished}}
</span>
</a>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 2ca5ac2912f..f0b44dfa6d8 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -1,29 +1,45 @@
<script>
-import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
+ import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+ import tooltip from '../../vue_shared/directives/tooltip';
+ import popover from '../../vue_shared/directives/popover';
-export default {
- props: {
- pipeline: {
- type: Object,
- required: true,
+ export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
+ },
},
- },
- components: {
- userAvatarLink,
- },
- directives: {
- tooltip,
- },
- computed: {
- user() {
- return this.pipeline.user;
+ components: {
+ userAvatarLink,
},
- },
-};
+ directives: {
+ tooltip,
+ popover,
+ },
+ computed: {
+ user() {
+ return this.pipeline.user;
+ },
+ popoverOptions() {
+ return {
+ html: true,
+ delay: { hide: 600 },
+ trigger: 'hover',
+ 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>`,
+ };
+ },
+ },
+ };
</script>
<template>
- <div class="table-section section-15 hidden-xs hidden-sm">
+ <div class="table-section section-15 hidden-xs hidden-sm pipeline-tags">
<a
:href="pipeline.path"
class="js-pipeline-url-link">
@@ -57,6 +73,13 @@ export default {
:title="pipeline.yaml_errors">
yaml invalid
</span>
+ <a
+ v-if="pipeline.flags.auto_devops"
+ class="js-pipeline-url-autodevops label label-info autodevops-badge"
+ v-popover="popoverOptions"
+ role="button">
+ Auto DevOps
+ </a>
<span
v-if="pipeline.flags.stuck"
class="js-pipeline-url-stuck label label-warning">
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 5df317a76bf..5e6d6b2fbdc 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -25,8 +25,8 @@
return {
endpoint: pipelinesData.endpoint,
- cssClass: pipelinesData.cssClass,
helpPagePath: pipelinesData.helpPagePath,
+ autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
@@ -139,7 +139,7 @@
};
</script>
<template>
- <div :class="cssClass">
+ <div class="pipelines-container">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
@@ -198,6 +198,7 @@
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
+ :auto-devops-help-path="autoDevopsPath"
/>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index 5088d92209f..7aa0c0e8a7f 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -17,6 +17,10 @@
required: false,
default: false,
},
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
+ },
},
components: {
pipelinesTableRowComponent,
@@ -54,6 +58,7 @@
:key="model.id"
:pipeline="model"
:update-graph-dropdown="updateGraphDropdown"
+ :auto-devops-help-path="autoDevopsHelpPath"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index c3f1c426d8a..5b9bb6c3750 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -25,6 +25,10 @@ export default {
required: false,
default: false,
},
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
+ },
},
components: {
asyncButtonComponent,
@@ -218,7 +222,10 @@ export default {
</div>
</div>
- <pipeline-url :pipeline="pipeline" />
+ <pipeline-url
+ :pipeline="pipeline"
+ :auto-devops-help-path="autoDevopsHelpPath"
+ />
<div class="table-section section-25">
<div
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 1c2100a1c25..fe6602259e2 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -53,10 +53,6 @@ import Cookies from 'js-cookie';
return _this.changeProject($(e.currentTarget).val());
};
})(this));
- return $('.js-projects-dropdown-toggle').on('click', function(e) {
- e.preventDefault();
- return $('.js-projects-dropdown').select2('open');
- });
};
Project.prototype.changeProject = function(url) {
@@ -126,11 +122,11 @@ import Cookies from 'js-cookie';
var $form = $dropdown.closest('form');
var $visit = $dropdown.data('visit');
- var shouldVisit = typeof $visit === 'undefined' ? true : $visit;
+ var shouldVisit = $visit ? true : $visit;
var action = $form.attr('action');
var divider = action.indexOf('?') === -1 ? '?' : '&';
if (shouldVisit) {
- gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
+ gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
}
}
}
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 1b4ed6be90a..fb01390f91c 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -5,48 +5,6 @@ import ProjectSelectComboButton from './project_select_combo_button';
(function() {
this.ProjectSelect = (function() {
function ProjectSelect() {
- $('.js-projects-dropdown-toggle').each(function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return $dropdown.glDropdown({
- filterable: true,
- filterRemote: true,
- search: {
- fields: ['name_with_namespace']
- },
- data: function(term, callback) {
- var finalCallback, projectsCallback;
- var orderBy = $dropdown.data('order-by');
- finalCallback = function(projects) {
- return callback(projects);
- };
- if (this.includeGroups) {
- projectsCallback = function(projects) {
- var groupsCallback;
- groupsCallback = function(groups) {
- var data;
- data = groups.concat(projects);
- return finalCallback(data);
- };
- return Api.groups(term, {}, groupsCallback);
- };
- } else {
- projectsCallback = finalCallback;
- }
- if (this.groupId) {
- return Api.groupProjects(this.groupId, term, projectsCallback);
- } else {
- return Api.projects(term, { order_by: orderBy }, projectsCallback);
- }
- },
- url: function(project) {
- return project.web_url;
- },
- text: function(project) {
- return project.name_with_namespace;
- }
- });
- });
$('.ajax-project-select').each(function(i, select) {
var placeholder;
this.groupId = $(select).data('group-id');
diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js
index f799d9d619a..99cea683d9a 100644
--- a/app/assets/javascripts/project_select_combo_button.js
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -4,17 +4,24 @@ export default class ProjectSelectComboButton {
constructor(select) {
this.projectSelectInput = $(select);
this.newItemBtn = $('.new-project-item-link');
- this.newItemBtnBaseText = this.newItemBtn.data('label');
- this.itemType = this.deriveItemTypeFromLabel();
+ this.resourceType = this.newItemBtn.data('type');
+ this.resourceLabel = this.newItemBtn.data('label');
+ this.formattedText = this.deriveTextVariants();
this.groupId = this.projectSelectInput.data('groupId');
-
this.bindEvents();
this.initLocalStorage();
}
bindEvents() {
this.projectSelectInput.siblings('.new-project-item-select-button')
- .on('click', this.openDropdown);
+ .on('click', e => this.openDropdown(e));
+
+ this.newItemBtn.on('click', (e) => {
+ if (!this.getProjectFromLocalStorage()) {
+ e.preventDefault();
+ this.openDropdown(e);
+ }
+ });
this.projectSelectInput.on('change', () => this.selectProject());
}
@@ -23,15 +30,14 @@ export default class ProjectSelectComboButton {
const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe();
if (localStorageIsSafe) {
- const itemTypeKebabed = this.newItemBtnBaseText.toLowerCase().split(' ').join('-');
-
- this.localStorageKey = ['group', this.groupId, itemTypeKebabed, 'recent-project'].join('-');
+ this.localStorageKey = ['group', this.groupId, this.formattedText.localStorageItemType, 'recent-project'].join('-');
this.setBtnTextFromLocalStorage();
}
}
- openDropdown() {
- $(this).siblings('.project-item-select').select2('open');
+ // eslint-disable-next-line class-methods-use-this
+ openDropdown(event) {
+ $(event.currentTarget).siblings('.project-item-select').select2('open');
}
selectProject() {
@@ -57,19 +63,12 @@ export default class ProjectSelectComboButton {
setNewItemBtnAttributes(project) {
if (project) {
this.newItemBtn.attr('href', project.url);
- this.newItemBtn.text(`${this.newItemBtnBaseText} in ${project.name}`);
- this.newItemBtn.enable();
+ this.newItemBtn.text(`${this.formattedText.defaultTextPrefix} in ${project.name}`);
} else {
- this.newItemBtn.text(`Select project to create ${this.itemType}`);
- this.newItemBtn.disable();
+ this.newItemBtn.text(`Select project to create ${this.formattedText.presetTextSuffix}`);
}
}
- deriveItemTypeFromLabel() {
- // label is either 'New issue' or 'New merge request'
- return this.newItemBtnBaseText.split(' ').slice(1).join(' ');
- }
-
getProjectFromLocalStorage() {
const projectString = localStorage.getItem(this.localStorageKey);
@@ -81,5 +80,19 @@ export default class ProjectSelectComboButton {
localStorage.setItem(this.localStorageKey, projectString);
}
+
+ deriveTextVariants() {
+ const defaultTextPrefix = this.resourceLabel;
+
+ // the trailing slice call depluralizes each of these strings (e.g. new-issues -> new-issue)
+ const localStorageItemType = `new-${this.resourceType.split('_').join('-').slice(0, -1)}`;
+ const presetTextSuffix = this.resourceType.split('_').join(' ').slice(0, -1);
+
+ return {
+ localStorageItemType, // new-issue / new-merge-request
+ defaultTextPrefix, // New issue / New merge request
+ presetTextSuffix, // issue / merge request
+ };
+ }
}
diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/project_visibility.js
new file mode 100644
index 00000000000..c3f5e8cb907
--- /dev/null
+++ b/app/assets/javascripts/project_visibility.js
@@ -0,0 +1,41 @@
+function setVisibilityOptions(namespaceSelector) {
+ if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) {
+ return;
+ }
+ const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex];
+ const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset;
+
+ document.querySelectorAll('.visibility-level-setting .radio').forEach((option) => {
+ const optionInput = option.querySelector('input[type=radio]');
+ const optionValue = optionInput ? optionInput.value : 0;
+ const optionTitle = option.querySelector('.option-title');
+ const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
+
+ // don't change anything if the option is restricted by admin
+ if (!option.classList.contains('restricted')) {
+ if (visibilityLevel < optionValue) {
+ option.classList.add('disabled');
+ optionInput.disabled = true;
+ const reason = option.querySelector('.option-disabled-reason');
+ if (reason) {
+ reason.innerHTML =
+ `This project cannot be ${optionName} because the visibility of
+ <a href="${showPath}">${name}</a> is ${visibility}. To make this project
+ ${optionName}, you must first <a href="${editPath}">change the visibility</a>
+ of the parent group.`;
+ }
+ } else {
+ option.classList.remove('disabled');
+ optionInput.disabled = false;
+ }
+ }
+ });
+}
+
+export default function initProjectVisibilitySelector() {
+ const namespaceSelector = document.querySelector('select.js-select-namespace');
+ if (namespaceSelector) {
+ $('.select2.js-select-namespace').on('change', () => setVisibilityOptions(namespaceSelector));
+ setVisibilityOptions(namespaceSelector);
+ }
+}
diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
new file mode 100644
index 00000000000..80c5d39f736
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
@@ -0,0 +1,104 @@
+<script>
+import projectFeatureToggle from './project_feature_toggle.vue';
+
+export default {
+ props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ options: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ value: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ disabledInput: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ components: {
+ projectFeatureToggle,
+ },
+
+ computed: {
+ featureEnabled() {
+ return this.value !== 0;
+ },
+
+ displayOptions() {
+ if (this.featureEnabled) {
+ return this.options;
+ }
+ return [
+ [0, 'Enable feature to choose access level'],
+ ];
+ },
+
+ displaySelectInput() {
+ return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2;
+ },
+ },
+
+ model: {
+ prop: 'value',
+ event: 'change',
+ },
+
+ methods: {
+ toggleFeature(featureEnabled) {
+ if (featureEnabled === false || this.options.length < 1) {
+ this.$emit('change', 0);
+ } else {
+ const [firstOptionValue] = this.options[this.options.length - 1];
+ this.$emit('change', firstOptionValue);
+ }
+ },
+
+ selectOption(e) {
+ this.$emit('change', Number(e.target.value));
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="project-feature-controls" :data-for="name">
+ <input
+ v-if="name"
+ type="hidden"
+ :name="name"
+ :value="value"
+ />
+ <project-feature-toggle
+ :value="featureEnabled"
+ @change="toggleFeature"
+ :disabledInput="disabledInput"
+ />
+ <div class="select-wrapper">
+ <select
+ class="form-control project-repo-select select-control"
+ @change="selectOption"
+ :disabled="displaySelectInput"
+ >
+ <option
+ v-for="[optionValue, optionName] in displayOptions"
+ :key="optionValue"
+ :value="optionValue"
+ :selected="optionValue === value"
+ >
+ {{optionName}}
+ </option>
+ </select>
+ <i aria-hidden="true" class="fa fa-chevron-down"></i>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue b/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue
new file mode 100644
index 00000000000..2403c60186a
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue
@@ -0,0 +1,51 @@
+<script>
+export default {
+ props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ value: {
+ type: Boolean,
+ required: true,
+ },
+ disabledInput: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ model: {
+ prop: 'value',
+ event: 'change',
+ },
+
+ methods: {
+ toggleFeature() {
+ if (!this.disabledInput) this.$emit('change', !this.value);
+ },
+ },
+};
+</script>
+
+<template>
+ <label class="toggle-wrapper">
+ <input
+ v-if="name"
+ type="hidden"
+ :name="name"
+ :value="value"
+ />
+ <button
+ type="button"
+ aria-label="Toggle"
+ class="project-feature-toggle"
+ data-enabled-text="Enabled"
+ data-disabled-text="Disabled"
+ :class="{ checked: value, disabled: disabledInput }"
+ @click="toggleFeature"
+ />
+ </label>
+</template>
diff --git a/app/assets/javascripts/projects/permissions/components/project_setting_row.vue b/app/assets/javascripts/projects/permissions/components/project_setting_row.vue
new file mode 100644
index 00000000000..6140d74fea8
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/components/project_setting_row.vue
@@ -0,0 +1,36 @@
+<script>
+export default {
+ props: {
+ label: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ helpPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ helpText: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="project-feature-row">
+ <label v-if="label" class="label-light">
+ {{label}}
+ <a v-if="helpPath" :href="helpPath" target="_blank">
+ <i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"></i>
+ </a>
+ </label>
+ <span v-if="helpText" class="help-block">
+ {{helpText}}
+ </span>
+ <slot />
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/permissions/components/settings_panel.vue b/app/assets/javascripts/projects/permissions/components/settings_panel.vue
new file mode 100644
index 00000000000..326d9105666
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/components/settings_panel.vue
@@ -0,0 +1,312 @@
+<script>
+import projectFeatureSetting from './project_feature_setting.vue';
+import projectFeatureToggle from './project_feature_toggle.vue';
+import projectSettingRow from './project_setting_row.vue';
+import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
+import { toggleHiddenClassBySelector } from '../external';
+
+export default {
+ props: {
+ currentSettings: {
+ type: Object,
+ required: true,
+ },
+ canChangeVisibilityLevel: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ allowedVisibilityOptions: {
+ type: Array,
+ required: false,
+ default: () => [0, 10, 20],
+ },
+ lfsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ registryAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ visibilityHelpPath: {
+ type: String,
+ required: false,
+ },
+ lfsHelpPath: {
+ type: String,
+ required: false,
+ },
+ registryHelpPath: {
+ type: String,
+ required: false,
+ },
+ },
+
+ data() {
+ const defaults = {
+ visibilityOptions,
+ visibilityLevel: visibilityOptions.PUBLIC,
+ issuesAccessLevel: 20,
+ repositoryAccessLevel: 20,
+ mergeRequestsAccessLevel: 20,
+ buildsAccessLevel: 20,
+ wikiAccessLevel: 20,
+ snippetsAccessLevel: 20,
+ containerRegistryEnabled: true,
+ lfsEnabled: true,
+ requestAccessEnabled: true,
+ highlightChangesClass: false,
+ };
+
+ return { ...defaults, ...this.currentSettings };
+ },
+
+ components: {
+ projectFeatureSetting,
+ projectFeatureToggle,
+ projectSettingRow,
+ },
+
+ computed: {
+ featureAccessLevelOptions() {
+ const options = [
+ [10, 'Only Project Members'],
+ ];
+ if (this.visibilityLevel !== visibilityOptions.PRIVATE) {
+ options.push([20, 'Everyone With Access']);
+ }
+ return options;
+ },
+
+ repoFeatureAccessLevelOptions() {
+ return this.featureAccessLevelOptions.filter(
+ ([value]) => value <= this.repositoryAccessLevel,
+ );
+ },
+
+ repositoryEnabled() {
+ return this.repositoryAccessLevel > 0;
+ },
+
+ visibilityLevelDescription() {
+ return visibilityLevelDescriptions[this.visibilityLevel];
+ },
+ },
+
+ methods: {
+ highlightChanges() {
+ this.highlightChangesClass = true;
+ this.$nextTick(() => {
+ this.highlightChangesClass = false;
+ });
+ },
+
+ visibilityAllowed(option) {
+ return this.allowedVisibilityOptions.includes(option);
+ },
+ },
+
+ watch: {
+ visibilityLevel(value, oldValue) {
+ if (value === visibilityOptions.PRIVATE) {
+ // when private, features are restricted to "only team members"
+ this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel);
+ this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel);
+ this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel);
+ this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
+ this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
+ this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
+ this.highlightChanges();
+ } else if (oldValue === visibilityOptions.PRIVATE) {
+ // if changing away from private, make enabled features more permissive
+ if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20;
+ if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20;
+ if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20;
+ if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
+ if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
+ if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
+ this.highlightChanges();
+ }
+ },
+
+ repositoryAccessLevel(value, oldValue) {
+ if (value < oldValue) {
+ // sub-features cannot have more premissive access level
+ this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value);
+ this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
+
+ if (value === 0) {
+ this.containerRegistryEnabled = false;
+ this.lfsEnabled = false;
+ }
+ } else if (oldValue === 0) {
+ this.mergeRequestsAccessLevel = value;
+ this.buildsAccessLevel = value;
+ this.containerRegistryEnabled = true;
+ this.lfsEnabled = true;
+ }
+ },
+
+ issuesAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.issues-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false);
+ },
+
+ mergeRequestsAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false);
+ },
+
+ buildsAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.builds-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
+ },
+ },
+};
+
+</script>
+
+<template>
+ <div>
+ <div class="project-visibility-setting">
+ <project-setting-row
+ label="Project visibility"
+ :help-path="visibilityHelpPath"
+ >
+ <div class="project-feature-controls">
+ <div class="select-wrapper">
+ <select
+ name="project[visibility_level]"
+ v-model="visibilityLevel"
+ class="form-control select-control"
+ :disabled="!canChangeVisibilityLevel"
+ >
+ <option
+ :value="visibilityOptions.PRIVATE"
+ :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)"
+ >
+ Private
+ </option>
+ <option
+ :value="visibilityOptions.INTERNAL"
+ :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)"
+ >
+ Internal
+ </option>
+ <option
+ :value="visibilityOptions.PUBLIC"
+ :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)"
+ >
+ Public
+ </option>
+ </select>
+ <i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
+ </div>
+ </div>
+ <span class="help-block">{{ visibilityLevelDescription }}</span>
+ <label v-if="visibilityLevel !== visibilityOptions.PUBLIC" class="request-access">
+ <input
+ type="hidden"
+ name="project[request_access_enabled]"
+ :value="requestAccessEnabled"
+ />
+ <input type="checkbox" v-model="requestAccessEnabled" />
+ Allow users to request access
+ </label>
+ </project-setting-row>
+ </div>
+ <div class="project-feature-settings" :class="{ 'highlight-changes': highlightChangesClass }">
+ <project-setting-row
+ label="Issues"
+ help-text="Lightweight issue tracking system for this project"
+ >
+ <project-feature-setting
+ name="project[project_feature_attributes][issues_access_level]"
+ :options="featureAccessLevelOptions"
+ v-model="issuesAccessLevel"
+ />
+ </project-setting-row>
+ <project-setting-row
+ label="Repository"
+ help-text="View and edit files in this project"
+ >
+ <project-feature-setting
+ name="project[project_feature_attributes][repository_access_level]"
+ :options="featureAccessLevelOptions"
+ v-model="repositoryAccessLevel"
+ />
+ </project-setting-row>
+ <div class="project-feature-setting-group">
+ <project-setting-row
+ label="Merge requests"
+ help-text="Submit changes to be merged upstream"
+ >
+ <project-feature-setting
+ name="project[project_feature_attributes][merge_requests_access_level]"
+ :options="repoFeatureAccessLevelOptions"
+ v-model="mergeRequestsAccessLevel"
+ :disabledInput="!repositoryEnabled"
+ />
+ </project-setting-row>
+ <project-setting-row
+ label="Pipelines"
+ help-text="Build, test, and deploy your changes"
+ >
+ <project-feature-setting
+ name="project[project_feature_attributes][builds_access_level]"
+ :options="repoFeatureAccessLevelOptions"
+ v-model="buildsAccessLevel"
+ :disabledInput="!repositoryEnabled"
+ />
+ </project-setting-row>
+ <project-setting-row
+ v-if="registryAvailable"
+ label="Container registry"
+ :help-path="registryHelpPath"
+ help-text="Every project can have its own space to store its Docker images"
+ >
+ <project-feature-toggle
+ name="project[container_registry_enabled]"
+ v-model="containerRegistryEnabled"
+ :disabledInput="!repositoryEnabled"
+ />
+ </project-setting-row>
+ <project-setting-row
+ v-if="lfsAvailable"
+ label="Git Large File Storage"
+ :help-path="lfsHelpPath"
+ help-text="Manages large files such as audio, video, and graphics files"
+ >
+ <project-feature-toggle
+ name="project[lfs_enabled]"
+ v-model="lfsEnabled"
+ :disabledInput="!repositoryEnabled"
+ />
+ </project-setting-row>
+ </div>
+ <project-setting-row
+ label="Wiki"
+ help-text="Pages for project documentation"
+ >
+ <project-feature-setting
+ name="project[project_feature_attributes][wiki_access_level]"
+ :options="featureAccessLevelOptions"
+ v-model="wikiAccessLevel"
+ />
+ </project-setting-row>
+ <project-setting-row
+ label="Snippets"
+ help-text="Share code pastes with others out of Git repository"
+ >
+ <project-feature-setting
+ name="project[project_feature_attributes][snippets_access_level]"
+ :options="featureAccessLevelOptions"
+ v-model="snippetsAccessLevel"
+ />
+ </project-setting-row>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/permissions/constants.js b/app/assets/javascripts/projects/permissions/constants.js
new file mode 100644
index 00000000000..ce47562f259
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/constants.js
@@ -0,0 +1,11 @@
+export const visibilityOptions = {
+ PRIVATE: 0,
+ INTERNAL: 10,
+ PUBLIC: 20,
+};
+
+export const visibilityLevelDescriptions = {
+ [visibilityOptions.PRIVATE]: 'The project is accessible only by members of the project. Access must be granted explicitly to each user.',
+ [visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.',
+ [visibilityOptions.PUBLIC]: 'The project can be accessed by anyone, regardless of authentication.',
+};
diff --git a/app/assets/javascripts/projects/permissions/external.js b/app/assets/javascripts/projects/permissions/external.js
new file mode 100644
index 00000000000..460af4a2111
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/external.js
@@ -0,0 +1,18 @@
+const selectorCache = [];
+
+// workaround since we don't have a polyfill for classList.toggle 2nd parameter
+export function toggleHiddenClass(element, hidden) {
+ if (hidden) {
+ element.classList.add('hidden');
+ } else {
+ element.classList.remove('hidden');
+ }
+}
+
+// hide external feature-specific settings when a given feature is disabled
+export function toggleHiddenClassBySelector(selector, hidden) {
+ if (!selectorCache[selector]) {
+ selectorCache[selector] = document.querySelectorAll(selector);
+ }
+ selectorCache[selector].forEach(elm => toggleHiddenClass(elm, hidden));
+}
diff --git a/app/assets/javascripts/projects/permissions/index.js b/app/assets/javascripts/projects/permissions/index.js
new file mode 100644
index 00000000000..dbde8dda634
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/index.js
@@ -0,0 +1,13 @@
+import Vue from 'vue';
+import settingsPanel from './components/settings_panel.vue';
+
+export default function initProjectPermissionsSettings() {
+ const mountPoint = document.querySelector('.js-project-permissions-form');
+ const componentPropsEl = document.querySelector('.js-project-permissions-form-data');
+ const componentProps = JSON.parse(componentPropsEl.innerHTML);
+
+ return new Vue({
+ el: mountPoint,
+ render: createElement => createElement(settingsPanel, { props: { ...componentProps } }),
+ });
+}
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 985521aef34..7f972b6f6ee 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -36,7 +36,7 @@ const bindEvents = () => {
$('.how_to_import_link').on('click', (e) => {
e.preventDefault();
- $('.how_to_import_link').next('.modal').show();
+ $(e.currentTarget).next('.modal').show();
});
$('.modal-header .close').on('click', () => {
diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue
new file mode 100644
index 00000000000..7606605be32
--- /dev/null
+++ b/app/assets/javascripts/projects_dropdown/components/app.vue
@@ -0,0 +1,157 @@
+<script>
+import bs from '../../breakpoints';
+import eventHub from '../event_hub';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+import projectsListFrequent from './projects_list_frequent.vue';
+import projectsListSearch from './projects_list_search.vue';
+
+import search from './search.vue';
+
+export default {
+ components: {
+ search,
+ loadingIcon,
+ projectsListFrequent,
+ projectsListSearch,
+ },
+ props: {
+ currentProject: {
+ type: Object,
+ required: true,
+ },
+ store: {
+ type: Object,
+ required: true,
+ },
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isLoadingProjects: false,
+ isFrequentsListVisible: false,
+ isSearchListVisible: false,
+ isLocalStorageFailed: false,
+ isSearchFailed: false,
+ searchQuery: '',
+ };
+ },
+ computed: {
+ frequentProjects() {
+ return this.store.getFrequentProjects();
+ },
+ searchProjects() {
+ return this.store.getSearchedProjects();
+ },
+ },
+ methods: {
+ toggleFrequentProjectsList(state) {
+ this.isLoadingProjects = !state;
+ this.isSearchListVisible = !state;
+ this.isFrequentsListVisible = state;
+ },
+ toggleSearchProjectsList(state) {
+ this.isLoadingProjects = !state;
+ this.isFrequentsListVisible = !state;
+ this.isSearchListVisible = state;
+ },
+ toggleLoader(state) {
+ this.isFrequentsListVisible = !state;
+ this.isSearchListVisible = !state;
+ this.isLoadingProjects = state;
+ },
+ fetchFrequentProjects() {
+ const screenSize = bs.getBreakpointSize();
+ if (this.searchQuery && (screenSize !== 'sm' && screenSize !== 'xs')) {
+ this.toggleSearchProjectsList(true);
+ } else {
+ this.toggleLoader(true);
+ this.isLocalStorageFailed = false;
+ const projects = this.service.getFrequentProjects();
+ if (projects) {
+ this.toggleFrequentProjectsList(true);
+ this.store.setFrequentProjects(projects);
+ } else {
+ this.isLocalStorageFailed = true;
+ this.toggleFrequentProjectsList(true);
+ this.store.setFrequentProjects([]);
+ }
+ }
+ },
+ fetchSearchedProjects(searchQuery) {
+ this.searchQuery = searchQuery;
+ this.toggleLoader(true);
+ this.service.getSearchedProjects(this.searchQuery)
+ .then(res => res.json())
+ .then((results) => {
+ this.toggleSearchProjectsList(true);
+ this.store.setSearchedProjects(results);
+ })
+ .catch(() => {
+ this.isSearchFailed = true;
+ this.toggleSearchProjectsList(true);
+ });
+ },
+ logCurrentProjectAccess() {
+ this.service.logProjectAccess(this.currentProject);
+ },
+ handleSearchClear() {
+ this.searchQuery = '';
+ this.toggleFrequentProjectsList(true);
+ this.store.clearSearchedProjects();
+ },
+ handleSearchFailure() {
+ this.isSearchFailed = true;
+ this.toggleSearchProjectsList(true);
+ },
+ },
+ created() {
+ if (this.currentProject.id) {
+ this.logCurrentProjectAccess();
+ }
+
+ eventHub.$on('dropdownOpen', this.fetchFrequentProjects);
+ eventHub.$on('searchProjects', this.fetchSearchedProjects);
+ eventHub.$on('searchCleared', this.handleSearchClear);
+ eventHub.$on('searchFailed', this.handleSearchFailure);
+ },
+ beforeDestroy() {
+ eventHub.$off('dropdownOpen', this.fetchFrequentProjects);
+ eventHub.$off('searchProjects', this.fetchSearchedProjects);
+ eventHub.$off('searchCleared', this.handleSearchClear);
+ eventHub.$off('searchFailed', this.handleSearchFailure);
+ },
+};
+</script>
+
+<template>
+ <div>
+ <search/>
+ <loading-icon
+ class="loading-animation prepend-top-20"
+ size="2"
+ v-if="isLoadingProjects"
+ :label="s__('ProjectsDropdown|Loading projects')"
+ />
+ <div
+ class="section-header"
+ v-if="isFrequentsListVisible"
+ >
+ {{ s__('ProjectsDropdown|Frequently visited') }}
+ </div>
+ <projects-list-frequent
+ v-if="isFrequentsListVisible"
+ :local-storage-failed="isLocalStorageFailed"
+ :projects="frequentProjects"
+ />
+ <projects-list-search
+ v-if="isSearchListVisible"
+ :search-failed="isSearchFailed"
+ :matcher="searchQuery"
+ :projects="searchProjects"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue
new file mode 100644
index 00000000000..093554cd0bc
--- /dev/null
+++ b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue
@@ -0,0 +1,57 @@
+<script>
+import { s__ } from '../../locale';
+import projectsListItem from './projects_list_item.vue';
+
+export default {
+ components: {
+ projectsListItem,
+ },
+ props: {
+ projects: {
+ type: Array,
+ required: true,
+ },
+ localStorageFailed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ isListEmpty() {
+ return this.projects.length === 0;
+ },
+ listEmptyMessage() {
+ return this.localStorageFailed ?
+ s__('ProjectsDropdown|This feature requires browser localStorage support') :
+ s__('ProjectsDropdown|Projects you visit often will appear here');
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="projects-list-frequent-container"
+ >
+ <ul
+ class="list-unstyled"
+ >
+ <li
+ class="section-empty"
+ v-if="isListEmpty"
+ >
+ {{listEmptyMessage}}
+ </li>
+ <projects-list-item
+ v-else
+ v-for="(project, index) in projects"
+ :key="index"
+ :project-id="project.id"
+ :project-name="project.name"
+ :namespace="project.namespace"
+ :web-url="project.webUrl"
+ :avatar-url="project.avatarUrl"
+ />
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
new file mode 100644
index 00000000000..fe5179de206
--- /dev/null
+++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
@@ -0,0 +1,96 @@
+<script>
+import identicon from '../../vue_shared/components/identicon.vue';
+
+export default {
+ components: {
+ identicon,
+ },
+ props: {
+ matcher: {
+ type: String,
+ required: false,
+ },
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ projectName: {
+ type: String,
+ required: true,
+ },
+ namespace: {
+ type: String,
+ required: true,
+ },
+ webUrl: {
+ type: String,
+ required: true,
+ },
+ avatarUrl: {
+ required: true,
+ validator(value) {
+ return value === null || typeof value === 'string';
+ },
+ },
+ },
+ computed: {
+ hasAvatar() {
+ return this.avatarUrl !== null;
+ },
+ highlightedProjectName() {
+ if (this.matcher) {
+ const matcherRegEx = new RegExp(this.matcher, 'gi');
+ const matches = this.projectName.match(matcherRegEx);
+
+ if (matches && matches.length > 0) {
+ return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`);
+ }
+ }
+ return this.projectName;
+ },
+ },
+};
+</script>
+
+<template>
+ <li
+ class="projects-list-item-container"
+ >
+ <a
+ class="clearfix"
+ :href="webUrl"
+ >
+ <div
+ class="project-item-avatar-container"
+ >
+ <img
+ v-if="hasAvatar"
+ class="avatar s32"
+ :src="avatarUrl"
+ />
+ <identicon
+ v-else
+ size-class="s32"
+ :entity-id=projectId
+ :entity-name="projectName"
+ />
+ </div>
+ <div
+ class="project-item-metadata-container"
+ >
+ <div
+ class="project-title"
+ :title="projectName"
+ v-html="highlightedProjectName"
+ >
+ </div>
+ <div
+ class="project-namespace"
+ :title="namespace"
+ >
+ {{namespace}}
+ </div>
+ </div>
+ </a>
+ </li>
+</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
new file mode 100644
index 00000000000..8d0c29177e6
--- /dev/null
+++ b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
@@ -0,0 +1,63 @@
+<script>
+import { s__ } from '../../locale';
+import projectsListItem from './projects_list_item.vue';
+
+export default {
+ components: {
+ projectsListItem,
+ },
+ props: {
+ matcher: {
+ type: String,
+ required: true,
+ },
+ projects: {
+ type: Array,
+ required: true,
+ },
+ searchFailed: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ isListEmpty() {
+ return this.projects.length === 0;
+ },
+ listEmptyMessage() {
+ return this.searchFailed ?
+ s__('ProjectsDropdown|Something went wrong on our end.') :
+ s__('ProjectsDropdown|Sorry, no projects matched your search');
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="projects-list-search-container"
+ >
+ <ul
+ class="list-unstyled"
+ >
+ <li
+ v-if="isListEmpty"
+ :class="{ 'section-failure': searchFailed }"
+ class="section-empty"
+ >
+ {{ listEmptyMessage }}
+ </li>
+ <projects-list-item
+ v-else
+ v-for="(project, index) in projects"
+ :key="index"
+ :project-id="project.id"
+ :project-name="project.name"
+ :namespace="project.namespace"
+ :web-url="project.webUrl"
+ :avatar-url="project.avatarUrl"
+ :matcher="matcher"
+ />
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue
new file mode 100644
index 00000000000..53bc76d0f2d
--- /dev/null
+++ b/app/assets/javascripts/projects_dropdown/components/search.vue
@@ -0,0 +1,64 @@
+<script>
+import _ from 'underscore';
+import eventHub from '../event_hub';
+
+export default {
+ data() {
+ return {
+ searchQuery: '',
+ };
+ },
+ watch: {
+ searchQuery() {
+ this.handleInput();
+ },
+ },
+ methods: {
+ setFocus() {
+ this.$refs.search.focus();
+ },
+ emitSearchEvents() {
+ if (this.searchQuery) {
+ eventHub.$emit('searchProjects', this.searchQuery);
+ } else {
+ eventHub.$emit('searchCleared');
+ }
+ },
+ /**
+ * Callback function within _.debounce is intentionally
+ * kept as ES5 `function() {}` instead of ES6 `() => {}`
+ * as it otherwise messes up function context
+ * and component reference is no longer accessible via `this`
+ */
+ // eslint-disable-next-line func-names
+ handleInput: _.debounce(function () {
+ this.emitSearchEvents();
+ }, 500),
+ },
+ mounted() {
+ eventHub.$on('dropdownOpen', this.setFocus);
+ },
+ beforeDestroy() {
+ eventHub.$off('dropdownOpen', this.setFocus);
+ },
+};
+</script>
+
+<template>
+ <div
+ class="search-input-container hidden-xs"
+ >
+ <input
+ type="search"
+ class="form-control"
+ ref="search"
+ v-model="searchQuery"
+ :placeholder="s__('ProjectsDropdown|Search your projects')"
+ />
+ <i
+ v-if="!searchQuery"
+ class="search-icon fa fa-fw fa-search"
+ aria-hidden="true"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects_dropdown/constants.js b/app/assets/javascripts/projects_dropdown/constants.js
new file mode 100644
index 00000000000..8937097184c
--- /dev/null
+++ b/app/assets/javascripts/projects_dropdown/constants.js
@@ -0,0 +1,10 @@
+export const FREQUENT_PROJECTS = {
+ MAX_COUNT: 20,
+ LIST_COUNT_DESKTOP: 5,
+ LIST_COUNT_MOBILE: 3,
+ ELIGIBLE_FREQUENCY: 3,
+};
+
+export const HOUR_IN_MS = 3600000;
+
+export const STORAGE_KEY = 'frequent-projects';
diff --git a/app/assets/javascripts/projects_dropdown/event_hub.js b/app/assets/javascripts/projects_dropdown/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/projects_dropdown/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js
new file mode 100644
index 00000000000..2660da3c558
--- /dev/null
+++ b/app/assets/javascripts/projects_dropdown/index.js
@@ -0,0 +1,68 @@
+import Vue from 'vue';
+
+import Translate from '../vue_shared/translate';
+import eventHub from './event_hub';
+import ProjectsService from './service/projects_service';
+import ProjectsStore from './store/projects_store';
+
+import projectsDropdownApp from './components/app.vue';
+
+Vue.use(Translate);
+
+document.addEventListener('DOMContentLoaded', () => {
+ const el = document.getElementById('js-projects-dropdown');
+ const navEl = document.getElementById('nav-projects-dropdown');
+
+ // Don't do anything if element doesn't exist (No projects dropdown)
+ // This is for when the user accesses GitLab without logging in
+ if (!el || !navEl) {
+ return;
+ }
+
+ $(navEl).on('show.bs.dropdown', (e) => {
+ const dropdownEl = $(e.currentTarget).find('.projects-dropdown-menu');
+ dropdownEl.one('transitionend', () => {
+ eventHub.$emit('dropdownOpen');
+ });
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ projectsDropdownApp,
+ },
+ data() {
+ const dataset = this.$options.el.dataset;
+ const store = new ProjectsStore();
+ const service = new ProjectsService(dataset.userName);
+
+ const project = {
+ id: Number(dataset.projectId),
+ name: dataset.projectName,
+ namespace: dataset.projectNamespace,
+ webUrl: dataset.projectWebUrl,
+ avatarUrl: dataset.projectAvatarUrl || null,
+ lastAccessedOn: Date.now(),
+ };
+
+ return {
+ store,
+ service,
+ state: store.state,
+ currentUserName: dataset.userName,
+ currentProject: project,
+ };
+ },
+ render(createElement) {
+ return createElement('projects-dropdown-app', {
+ props: {
+ currentUserName: this.currentUserName,
+ currentProject: this.currentProject,
+ store: this.store,
+ service: this.service,
+ },
+ });
+ },
+ });
+});
diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js
new file mode 100644
index 00000000000..fad956b4c26
--- /dev/null
+++ b/app/assets/javascripts/projects_dropdown/service/projects_service.js
@@ -0,0 +1,132 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+import bp from '../../breakpoints';
+import Api from '../../api';
+import AccessorUtilities from '../../lib/utils/accessor';
+
+import { FREQUENT_PROJECTS, HOUR_IN_MS, STORAGE_KEY } from '../constants';
+
+Vue.use(VueResource);
+
+export default class ProjectsService {
+ constructor(currentUserName) {
+ this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
+ this.currentUserName = currentUserName;
+ this.storageKey = `${this.currentUserName}/${STORAGE_KEY}`;
+ this.projectsPath = Vue.resource(Api.buildUrl(Api.projectsPath));
+ }
+
+ getSearchedProjects(searchQuery) {
+ return this.projectsPath.get({
+ simple: false,
+ per_page: 20,
+ membership: !!gon.current_user_id,
+ order_by: 'last_activity_at',
+ search: searchQuery,
+ });
+ }
+
+ getFrequentProjects() {
+ if (this.isLocalStorageAvailable) {
+ return this.getTopFrequentProjects();
+ }
+ return null;
+ }
+
+ logProjectAccess(project) {
+ let matchFound = false;
+ let storedFrequentProjects;
+
+ if (this.isLocalStorageAvailable) {
+ const storedRawProjects = localStorage.getItem(this.storageKey);
+
+ // Check if there's any frequent projects list set
+ if (!storedRawProjects) {
+ // No frequent projects list set, set one up.
+ storedFrequentProjects = [];
+ storedFrequentProjects.push({ ...project, frequency: 1 });
+ } else {
+ // Check if project is already present in frequents list
+ // When found, update metadata of it.
+ storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => {
+ if (projectItem.id === project.id) {
+ matchFound = true;
+ const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
+ const updatedProject = {
+ ...project,
+ frequency: projectItem.frequency,
+ lastAccessedOn: projectItem.lastAccessedOn,
+ };
+
+ // Check if duration since last access of this project
+ // is over an hour
+ if (diff > 1) {
+ return {
+ ...updatedProject,
+ frequency: updatedProject.frequency + 1,
+ lastAccessedOn: Date.now(),
+ };
+ }
+
+ return {
+ ...updatedProject,
+ };
+ }
+
+ return projectItem;
+ });
+
+ // Check whether currently logged project is present in frequents list
+ if (!matchFound) {
+ // We always keep size of frequents collection to 20 projects
+ // out of which only 5 projects with
+ // highest value of `frequency` and most recent `lastAccessedOn`
+ // are shown in projects dropdown
+ if (storedFrequentProjects.length === FREQUENT_PROJECTS.MAX_COUNT) {
+ storedFrequentProjects.shift(); // Remove an item from head of array
+ }
+
+ storedFrequentProjects.push({ ...project, frequency: 1 });
+ }
+ }
+
+ localStorage.setItem(this.storageKey, JSON.stringify(storedFrequentProjects));
+ }
+ }
+
+ getTopFrequentProjects() {
+ const storedFrequentProjects = JSON.parse(localStorage.getItem(this.storageKey));
+ let frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_DESKTOP;
+
+ if (!storedFrequentProjects) {
+ return [];
+ }
+
+ if (bp.getBreakpointSize() === 'sm' ||
+ bp.getBreakpointSize() === 'xs') {
+ frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
+ }
+
+ const frequentProjects = storedFrequentProjects
+ .filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY);
+
+ // Sort all frequent projects in decending order of frequency
+ // and then by lastAccessedOn with recent most first
+ frequentProjects.sort((projectA, projectB) => {
+ if (projectA.frequency < projectB.frequency) {
+ return 1;
+ } else if (projectA.frequency > projectB.frequency) {
+ return -1;
+ } else if (projectA.lastAccessedOn < projectB.lastAccessedOn) {
+ return 1;
+ } else if (projectA.lastAccessedOn > projectB.lastAccessedOn) {
+ return -1;
+ }
+
+ return 0;
+ });
+
+ return _.first(frequentProjects, frequentProjectsCount);
+ }
+}
diff --git a/app/assets/javascripts/projects_dropdown/store/projects_store.js b/app/assets/javascripts/projects_dropdown/store/projects_store.js
new file mode 100644
index 00000000000..ffefbe693f4
--- /dev/null
+++ b/app/assets/javascripts/projects_dropdown/store/projects_store.js
@@ -0,0 +1,33 @@
+export default class ProjectsStore {
+ constructor() {
+ this.state = {};
+ this.state.frequentProjects = [];
+ this.state.searchedProjects = [];
+ }
+
+ setFrequentProjects(rawProjects) {
+ this.state.frequentProjects = rawProjects;
+ }
+
+ getFrequentProjects() {
+ return this.state.frequentProjects;
+ }
+
+ setSearchedProjects(rawProjects) {
+ this.state.searchedProjects = rawProjects.map(rawProject => ({
+ id: rawProject.id,
+ name: rawProject.name,
+ namespace: rawProject.name_with_namespace,
+ webUrl: rawProject.web_url,
+ avatarUrl: rawProject.avatar_url,
+ }));
+ }
+
+ getSearchedProjects() {
+ return this.state.searchedProjects;
+ }
+
+ clearSearchedProjects() {
+ this.state.searchedProjects = [];
+ }
+}
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
index 2c3a9cacd38..bcdc0fd67b8 100644
--- a/app/assets/javascripts/render_gfm.js
+++ b/app/assets/javascripts/render_gfm.js
@@ -11,7 +11,5 @@
return this;
};
- $(document).on('ready load', function() {
- return $('body').renderGFM();
- });
+ $(() => $('body').renderGFM());
}).call(window);
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
index 703da749ad3..d6c864cb976 100644
--- a/app/assets/javascripts/repo/components/repo.vue
+++ b/app/assets/javascripts/repo/components/repo.vue
@@ -14,13 +14,13 @@ export default {
data: () => Store,
mixins: [RepoMixin],
components: {
- 'repo-sidebar': RepoSidebar,
- 'repo-tabs': RepoTabs,
- 'repo-file-buttons': RepoFileButtons,
+ RepoSidebar,
+ RepoTabs,
+ RepoFileButtons,
'repo-editor': MonacoLoaderHelper.repoEditorLoader,
- 'repo-commit-section': RepoCommitSection,
- 'popup-dialog': PopupDialog,
- 'repo-preview': RepoPreview,
+ RepoCommitSection,
+ PopupDialog,
+ RepoPreview,
},
mounted() {
@@ -28,12 +28,12 @@ export default {
},
methods: {
- dialogToggled(toggle) {
+ toggleDialogOpen(toggle) {
this.dialog.open = toggle;
},
dialogSubmitted(status) {
- this.dialog.open = false;
+ this.toggleDialogOpen(false);
this.dialog.status = status;
},
@@ -43,21 +43,28 @@ export default {
</script>
<template>
-<div class="repository-view tree-content-holder">
- <repo-sidebar/><div class="panel-right" :class="{'edit-mode': editMode}">
- <repo-tabs/>
- <component :is="currentBlobView" class="blob-viewer-container"></component>
- <repo-file-buttons/>
+ <div class="repository-view">
+ <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isMini}">
+ <repo-sidebar/>
+ <div v-if="isMini"
+ class="panel-right"
+ :class="{'edit-mode': editMode}">
+ <repo-tabs/>
+ <component
+ :is="currentBlobView"
+ class="blob-viewer-container"/>
+ <repo-file-buttons/>
+ </div>
+ </div>
+ <repo-commit-section/>
+ <popup-dialog
+ v-show="dialog.open"
+ :primary-button-label="__('Discard changes')"
+ kind="warning"
+ :title="__('Are you sure?')"
+ :body="__('Are you sure you want to discard your changes?')"
+ @toggle="toggleDialogOpen"
+ @submit="dialogSubmitted"
+ />
</div>
- <repo-commit-section/>
- <popup-dialog
- :primary-button-label="__('Discard changes')"
- :open="dialog.open"
- kind="warning"
- :title="__('Are you sure?')"
- :body="__('Are you sure you want to discard your changes?')"
- @toggle="dialogToggled"
- @submit="dialogSubmitted"
- />
-</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue
index bd83f80c928..1282828b504 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/repo/components/repo_commit_section.vue
@@ -2,18 +2,20 @@
/* global Flash */
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
-import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
-const RepoCommitSection = {
+export default {
data: () => Store,
mixins: [RepoMixin],
computed: {
+ showCommitable() {
+ return this.isCommitable && this.changedFiles.length;
+ },
+
branchPaths() {
- const branch = Helper.getBranch();
- return this.changedFiles.map(f => Helper.getFilePathFromFullPath(f.url, branch));
+ return this.changedFiles.map(f => f.path);
},
cantCommitYet() {
@@ -28,11 +30,10 @@ const RepoCommitSection = {
methods: {
makeCommit() {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
- const branch = Helper.getBranch();
const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({
action: 'update',
- file_path: Helper.getFilePathFromFullPath(f.url, branch),
+ file_path: f.path,
content: f.newContent,
}));
const payload = {
@@ -41,57 +42,88 @@ const RepoCommitSection = {
actions,
};
Store.submitCommitsLoading = true;
- Service.commitFiles(payload, this.resetCommitState);
+ Service.commitFiles(payload)
+ .then(this.resetCommitState)
+ .catch(() => Flash('An error occured while committing your changes'));
},
resetCommitState() {
this.submitCommitsLoading = false;
this.changedFiles = [];
- this.openedFiles = [];
this.commitMessage = '';
this.editMode = false;
- $('html, body').animate({ scrollTop: 0 }, 'fast');
+ window.scrollTo(0, 0);
},
},
};
-
-export default RepoCommitSection;
</script>
<template>
-<div id="commit-area" v-if="isCommitable && changedFiles.length" >
- <form class="form-horizontal">
+<div
+ v-if="showCommitable"
+ id="commit-area">
+ <form
+ class="form-horizontal"
+ @submit.prevent="makeCommit">
<fieldset>
<div class="form-group">
- <label class="col-md-4 control-label staged-files">Staged files ({{changedFiles.length}})</label>
- <div class="col-md-4">
+ <label class="col-md-4 control-label staged-files">
+ Staged files ({{changedFiles.length}})
+ </label>
+ <div class="col-md-6">
<ul class="list-unstyled changed-files">
- <li v-for="file in branchPaths" :key="file.id">
- <span class="help-block">{{file}}</span>
+ <li
+ v-for="branchPath in branchPaths"
+ :key="branchPath">
+ <span class="help-block">
+ {{branchPath}}
+ </span>
</li>
</ul>
</div>
</div>
- <!-- Textarea
- -->
<div class="form-group">
- <label class="col-md-4 control-label" for="commit-message">Commit message</label>
- <div class="col-md-4">
- <textarea class="form-control" id="commit-message" name="commit-message" v-model="commitMessage"></textarea>
+ <label
+ class="col-md-4 control-label"
+ for="commit-message">
+ Commit message
+ </label>
+ <div class="col-md-6">
+ <textarea
+ id="commit-message"
+ class="form-control"
+ name="commit-message"
+ v-model="commitMessage">
+ </textarea>
</div>
</div>
- <!-- Button Drop Down
- -->
<div class="form-group target-branch">
- <label class="col-md-4 control-label" for="target-branch">Target branch</label>
- <div class="col-md-4">
- <span class="help-block">{{targetBranch}}</span>
+ <label
+ class="col-md-4 control-label"
+ for="target-branch">
+ Target branch
+ </label>
+ <div class="col-md-6">
+ <span class="help-block">
+ {{targetBranch}}
+ </span>
</div>
</div>
- <div class="col-md-offset-4 col-md-4">
- <button type="submit" :disabled="cantCommitYet" class="btn btn-success submit-commit" @click.prevent="makeCommit">
- <i class="fa fa-spinner fa-spin" v-if="submitCommitsLoading"></i>
- <span class="commit-summary">Commit {{changedFiles.length}} {{filePluralize}}</span>
+ <div class="col-md-offset-4 col-md-6">
+ <button
+ ref="submitCommit"
+ type="submit"
+ :disabled="cantCommitYet"
+ class="btn btn-success">
+ <i
+ v-if="submitCommitsLoading"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"
+ aria-label="loading">
+ </i>
+ <span class="commit-summary">
+ Commit {{changedFiles.length}} {{filePluralize}}
+ </span>
</button>
</div>
</fieldset>
diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue
index e954fd38fc9..29b76975561 100644
--- a/app/assets/javascripts/repo/components/repo_edit_button.vue
+++ b/app/assets/javascripts/repo/components/repo_edit_button.vue
@@ -10,12 +10,15 @@ export default {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
- buttonIcon() {
- return this.editMode ? [] : ['fa', 'fa-pencil'];
+ showButton() {
+ return this.isCommitable &&
+ !this.activeFile.render_error &&
+ !this.binary &&
+ this.openedFiles.length;
},
},
methods: {
- editClicked() {
+ editCancelClicked() {
if (this.changedFiles.length) {
this.dialog.open = true;
return;
@@ -23,27 +26,33 @@ 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() {
- if (this.editMode) {
- $('.project-refs-form').addClass('disabled');
- $('.fa-long-arrow-right').show();
- $('.project-refs-target-form').show();
- } else {
- $('.project-refs-form').removeClass('disabled');
- $('.fa-long-arrow-right').hide();
- $('.project-refs-target-form').hide();
- }
+ this.toggleProjectRefsForm();
},
},
};
</script>
<template>
-<button class="btn btn-default" @click.prevent="editClicked" v-cloak v-if="isCommitable && !activeFile.render_error" :disabled="binary">
- <i :class="buttonIcon"></i>
- <span>{{buttonLabel}}</span>
+<button
+ v-if="showButton"
+ class="btn btn-default"
+ type="button"
+ @click.prevent="editCancelClicked">
+ <i
+ v-if="!editMode"
+ class="fa fa-pencil"
+ aria-hidden="true">
+ </i>
+ <span>
+ {{buttonLabel}}
+ </span>
</button>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
index fd1a21e15b4..96d6a75bb61 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -8,38 +8,39 @@ const RepoEditor = {
data: () => Store,
destroyed() {
- // this.monacoInstance.getModels().forEach((m) => {
- // m.dispose();
- // });
- this.monacoInstance.destroy();
+ if (Helper.monacoInstance) {
+ Helper.monacoInstance.destroy();
+ }
},
mounted() {
Service.getRaw(this.activeFile.raw_path)
- .then((rawResponse) => {
- Store.blobRaw = rawResponse.data;
- Helper.findOpenedFileFromActive().plain = rawResponse.data;
+ .then((rawResponse) => {
+ Store.blobRaw = rawResponse.data;
+ Store.activeFile.plain = rawResponse.data;
- const monacoInstance = this.monaco.editor.create(this.$el, {
- model: null,
- readOnly: false,
- contextmenu: false,
- });
+ const monacoInstance = Helper.monaco.editor.create(this.$el, {
+ model: null,
+ readOnly: false,
+ contextmenu: false,
+ });
- Store.monacoInstance = monacoInstance;
+ Helper.monacoInstance = monacoInstance;
- this.addMonacoEvents();
+ this.addMonacoEvents();
- const languages = this.monaco.languages.getLanguages();
- const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
- this.showHide();
- const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
-
- this.monacoInstance.setModel(newModel);
- }).catch(Helper.loadingError);
+ this.setupEditor();
+ })
+ .catch(Helper.loadingError);
},
methods: {
+ setupEditor() {
+ this.showHide();
+
+ Helper.setMonacoModelFromLanguage();
+ },
+
showHide() {
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
this.$el.style.display = 'none';
@@ -49,41 +50,36 @@ const RepoEditor = {
},
addMonacoEvents() {
- this.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
- this.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
+ Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
+ Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
},
onMonacoEditorKeysPressed() {
- Store.setActiveFileContents(this.monacoInstance.getValue());
+ Store.setActiveFileContents(Helper.monacoInstance.getValue());
},
onMonacoEditorMouseUp(e) {
+ if (!e.target.position) return;
const lineNumber = e.target.position.lineNumber;
- if (e.target.element.className === 'line-numbers') {
+ if (e.target.element.classList.contains('line-numbers')) {
location.hash = `L${lineNumber}`;
Store.activeLine = lineNumber;
+
+ Helper.monacoInstance.setPosition({
+ lineNumber: this.activeLine,
+ column: 1,
+ });
}
},
},
watch: {
- activeLine() {
- this.monacoInstance.setPosition({
- lineNumber: this.activeLine,
- column: 1,
- });
- },
-
- activeFileLabel() {
- this.showHide();
- },
-
dialog: {
handler(obj) {
const newObj = obj;
if (newObj.status) {
newObj.status = false;
- this.openedFiles.map((file) => {
+ this.openedFiles = this.openedFiles.map((file) => {
const f = file;
if (f.active) {
this.blobRaw = f.plain;
@@ -94,35 +90,21 @@ const RepoEditor = {
return f;
});
this.editMode = false;
+ Store.toggleBlobView();
}
},
deep: true,
},
- isTree() {
- this.showHide();
- },
-
- openedFiles() {
- this.showHide();
- },
-
- binary() {
- this.showHide();
- },
-
blobRaw() {
- this.showHide();
-
- if (this.isTree) return;
-
- this.monacoInstance.setModel(null);
-
- const languages = this.monaco.languages.getLanguages();
- const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
- const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
-
- this.monacoInstance.setModel(newModel);
+ if (Helper.monacoInstance && !this.isTree) {
+ this.setupEditor();
+ }
+ },
+ },
+ computed: {
+ shouldHideEditor() {
+ return !this.openedFiles.length || (this.binary && !this.activeFile.raw);
},
},
};
@@ -131,5 +113,5 @@ export default RepoEditor;
</script>
<template>
-<div id="ide"></div>
+<div id="ide" v-if='!shouldHideEditor'></div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue
index f604bc22a26..20ebf840774 100644
--- a/app/assets/javascripts/repo/components/repo_file.vue
+++ b/app/assets/javascripts/repo/components/repo_file.vue
@@ -33,6 +33,26 @@ const RepoFile = {
canShowFile() {
return !this.loading.tree || this.hasFiles;
},
+
+ fileIcon() {
+ const classObj = {
+ 'fa-spinner fa-spin': this.file.loading,
+ [this.file.icon]: !this.file.loading,
+ };
+ return classObj;
+ },
+
+ fileIndentation() {
+ return {
+ 'margin-left': `${this.file.level * 10}px`,
+ };
+ },
+
+ activeFileClass() {
+ return {
+ active: this.activeFile.url === this.file.url,
+ };
+ },
},
methods: {
@@ -46,21 +66,42 @@ export default RepoFile;
</script>
<template>
-<tr class="file" v-if="canShowFile" :class="{'active': activeFile.url === file.url}">
- <td @click.prevent="linkClicked(file)">
- <i class="fa file-icon" v-if="!file.loading" :class="file.icon" :style="{'margin-left': file.level * 10 + 'px'}"></i>
- <i class="fa fa-spinner fa-spin" v-if="file.loading" :style="{'margin-left': file.level * 10 + 'px'}"></i>
- <a :href="file.url" class="repo-file-name" :title="file.url">{{file.name}}</a>
+<tr
+ v-if="canShowFile"
+ class="file"
+ :class="activeFileClass"
+ @click.prevent="linkClicked(file)">
+ <td>
+ <i
+ class="fa fa-fw file-icon"
+ :class="fileIcon"
+ :style="fileIndentation"
+ aria-label="file icon">
+ </i>
+ <a
+ :href="file.url"
+ class="repo-file-name"
+ :title="file.url">
+ {{file.name}}
+ </a>
</td>
- <td v-if="!isMini" class="hidden-sm hidden-xs">
- <div class="commit-message">
- <a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a>
- </div>
- </td>
+ <template v-if="!isMini">
+ <td class="hidden-sm hidden-xs">
+ <div class="commit-message">
+ <a @click.stop :href="file.lastCommitUrl">
+ {{file.lastCommitMessage}}
+ </a>
+ </div>
+ </td>
- <td v-if="!isMini" class="hidden-xs">
- <span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)">{{timeFormated(file.lastCommitUpdate)}}</span>
- </td>
+ <td class="hidden-xs">
+ <span
+ class="commit-update"
+ :title="tooltipTitle(file.lastCommitUpdate)">
+ {{timeFormated(file.lastCommitUpdate)}}
+ </span>
+ </td>
+ </template>
</tr>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue
index 628d02ca704..e43ef366f47 100644
--- a/app/assets/javascripts/repo/components/repo_file_buttons.vue
+++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue
@@ -15,7 +15,7 @@ const RepoFileButtons = {
},
canPreview() {
- return Helper.isKindaBinary();
+ return Helper.isRenderable();
},
},
@@ -28,15 +28,42 @@ export default RepoFileButtons;
</script>
<template>
-<div id="repo-file-buttons" v-if="isMini">
- <a :href="activeFile.raw_path" target="_blank" class="btn btn-default raw" rel="noopener noreferrer">{{rawDownloadButtonLabel}}</a>
+ <div id="repo-file-buttons">
+ <a
+ :href="activeFile.raw_path"
+ target="_blank"
+ class="btn btn-default raw"
+ rel="noopener noreferrer">
+ {{rawDownloadButtonLabel}}
+ </a>
- <div class="btn-group" role="group" aria-label="File actions">
- <a :href="activeFile.blame_path" class="btn btn-default blame">Blame</a>
- <a :href="activeFile.commits_path" class="btn btn-default history">History</a>
- <a :href="activeFile.permalink" class="btn btn-default permalink">Permalink</a>
- </div>
+ <div
+ class="btn-group"
+ role="group"
+ aria-label="File actions">
+ <a
+ :href="activeFile.blame_path"
+ class="btn btn-default blame">
+ Blame
+ </a>
+ <a
+ :href="activeFile.commits_path"
+ class="btn btn-default history">
+ History
+ </a>
+ <a
+ :href="activeFile.permalink"
+ class="btn btn-default permalink">
+ Permalink
+ </a>
+ </div>
- <a href="#" v-if="canPreview" @click.prevent="rawPreviewToggle" class="btn btn-default preview">{{activeFileLabel}}</a>
-</div>
+ <a
+ v-if="canPreview"
+ href="#"
+ @click.prevent="rawPreviewToggle"
+ class="btn btn-default preview">
+ {{activeFileLabel}}
+ </a>
+ </div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_file_options.vue b/app/assets/javascripts/repo/components/repo_file_options.vue
index ba53ce0eecc..6a15755f029 100644
--- a/app/assets/javascripts/repo/components/repo_file_options.vue
+++ b/app/assets/javascripts/repo/components/repo_file_options.vue
@@ -17,7 +17,7 @@ export default RepoFileOptions;
</script>
<template>
-<tr v-if="isMini" class="repo-file-options">
+ <tr v-if="isMini" class="repo-file-options">
<td>
<span class="title">{{projectName}}</span>
</td>
diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue
index 38e9f16d041..bc8c64c8362 100644
--- a/app/assets/javascripts/repo/components/repo_loading_file.vue
+++ b/app/assets/javascripts/repo/components/repo_loading_file.vue
@@ -18,9 +18,15 @@ const RepoLoadingFile = {
},
},
+ computed: {
+ showGhostLines() {
+ return this.loading.tree && !this.hasFiles;
+ },
+ },
+
methods: {
lineOfCode(n) {
- return `line-of-code-${n}`;
+ return `skeleton-line-${n}`;
},
},
};
@@ -29,23 +35,42 @@ export default RepoLoadingFile;
</script>
<template>
-<tr v-if="loading.tree && !hasFiles" class="loading-file">
- <td>
- <div class="animation-container animation-container-small">
- <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
- </div>
- </td>
+ <tr
+ v-if="showGhostLines"
+ class="loading-file">
+ <td>
+ <div
+ class="animation-container animation-container-small">
+ <div
+ v-for="n in 6"
+ :key="n"
+ :class="lineOfCode(n)">
+ </div>
+ </div>
+ </td>
- <td v-if="!isMini" class="hidden-sm hidden-xs">
- <div class="animation-container">
- <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
- </div>
- </td>
+ <td
+ v-if="!isMini"
+ class="hidden-sm hidden-xs">
+ <div class="animation-container">
+ <div
+ v-for="n in 6"
+ :key="n"
+ :class="lineOfCode(n)">
+ </div>
+ </div>
+ </td>
- <td v-if="!isMini" class="hidden-xs">
- <div class="animation-container animation-container-small">
- <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
- </div>
- </td>
-</tr>
+ <td
+ v-if="!isMini"
+ class="hidden-xs">
+ <div class="animation-container animation-container-small">
+ <div
+ v-for="n in 6"
+ :key="n"
+ :class="lineOfCode(n)">
+ </div>
+ </div>
+ </td>
+ </tr>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue
index 6a0d684052f..bbdbdc61e38 100644
--- a/app/assets/javascripts/repo/components/repo_prev_directory.vue
+++ b/app/assets/javascripts/repo/components/repo_prev_directory.vue
@@ -1,4 +1,6 @@
<script>
+import RepoMixin from '../mixins/repo_mixin';
+
const RepoPreviousDirectory = {
props: {
prevUrl: {
@@ -7,6 +9,14 @@ const RepoPreviousDirectory = {
},
},
+ mixins: [RepoMixin],
+
+ computed: {
+ colSpanCondition() {
+ return this.isMini ? undefined : 3;
+ },
+ },
+
methods: {
linkClicked(file) {
this.$emit('linkclicked', file);
@@ -19,8 +29,10 @@ export default RepoPreviousDirectory;
<template>
<tr class="prev-directory">
- <td colspan="3">
- <a :href="prevUrl" @click.prevent="linkClicked(prevUrl)">..</a>
+ <td
+ :colspan="colSpanCondition"
+ @click.prevent="linkClicked(prevUrl)">
+ <a :href="prevUrl">..</a>
</td>
</tr>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue
index d8de022335b..2200754cbef 100644
--- a/app/assets/javascripts/repo/components/repo_preview.vue
+++ b/app/assets/javascripts/repo/components/repo_preview.vue
@@ -4,7 +4,7 @@ import Store from '../stores/repo_store';
export default {
data: () => Store,
mounted() {
- $(this.$el).find('.file-content').syntaxHighlight();
+ this.highlightFile();
},
computed: {
html() {
@@ -12,10 +12,16 @@ export default {
},
},
+ methods: {
+ highlightFile() {
+ $(this.$el).find('.file-content').syntaxHighlight();
+ },
+ },
+
watch: {
html() {
this.$nextTick(() => {
- $(this.$el).find('.file-content').syntaxHighlight();
+ this.highlightFile();
});
},
},
@@ -24,9 +30,23 @@ export default {
<template>
<div>
- <div v-if="!activeFile.render_error" v-html="activeFile.html"></div>
- <div v-if="activeFile.render_error" class="vertical-center render-error">
- <p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p>
+ <div
+ v-if="!activeFile.render_error"
+ v-html="activeFile.html">
+ </div>
+ <div
+ v-else-if="activeFile.tooLarge"
+ class="vertical-center render-error">
+ <p class="text-center">
+ The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.
+ </p>
+ </div>
+ <div
+ 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.
+ </p>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
index d6d832efc49..3414128526d 100644
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ b/app/assets/javascripts/repo/components/repo_sidebar.vue
@@ -8,7 +8,7 @@ import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue';
import RepoMixin from '../mixins/repo_mixin';
-const RepoSidebar = {
+export default {
mixins: [RepoMixin],
components: {
'repo-file-options': RepoFileOptions,
@@ -33,40 +33,36 @@ const RepoSidebar = {
});
},
- linkClicked(clickedFile) {
- let url = '';
+ fileClicked(clickedFile) {
let file = clickedFile;
- if (typeof file === 'object') {
- file.loading = true;
- if (file.type === 'tree' && file.opened) {
- file = Store.removeChildFilesOfTree(file);
- file.loading = false;
- } else {
- url = file.url;
- Service.url = url;
- // I need to refactor this to do the `then` here.
- // Not a callback. For now this is good enough.
- // it works.
- Helper.getContent(file, () => {
+ if (file.loading) return;
+ file.loading = true;
+ if (file.type === 'tree' && file.opened) {
+ file = Store.removeChildFilesOfTree(file);
+ file.loading = false;
+ } else {
+ Service.url = file.url;
+ Helper.getContent(file)
+ .then(() => {
file.loading = false;
Helper.scrollTabsRight();
- });
- }
- } else if (typeof file === 'string') {
- // go back
- url = file;
- Service.url = url;
- Helper.getContent(null, () => Helper.scrollTabsRight());
+ })
+ .catch(Helper.loadingError);
}
},
+
+ goToPreviousDirectoryClicked(prevURL) {
+ Service.url = prevURL;
+ Helper.getContent(null)
+ .then(() => Helper.scrollTabsRight())
+ .catch(Helper.loadingError);
+ },
},
};
-
-export default RepoSidebar;
</script>
<template>
-<div id="sidebar" :class="{'sidebar-mini' : isMini}" v-cloak>
+<div id="sidebar" :class="{'sidebar-mini' : isMini}">
<table class="table">
<thead v-if="!isMini">
<tr>
@@ -78,26 +74,29 @@ export default RepoSidebar;
<tbody>
<repo-file-options
:is-mini="isMini"
- :project-name="projectName"/>
+ :project-name="projectName"
+ />
<repo-previous-directory
v-if="isRoot"
:prev-url="prevURL"
- @linkclicked="linkClicked(prevURL)"/>
+ @linkclicked="goToPreviousDirectoryClicked(prevURL)"/>
<repo-loading-file
v-for="n in 5"
:key="n"
:loading="loading"
:has-files="!!files.length"
- :is-mini="isMini"/>
+ :is-mini="isMini"
+ />
<repo-file
v-for="file in files"
:key="file.id"
:file="file"
:is-mini="isMini"
- @linkclicked="linkClicked(file)"
+ @linkclicked="fileClicked(file)"
:is-tree="isTree"
:has-files="!!files.length"
- :active-file="activeFile"/>
+ :active-file="activeFile"
+ />
</tbody>
</table>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue
index 712d64c236f..0d0c34ec741 100644
--- a/app/assets/javascripts/repo/components/repo_tab.vue
+++ b/app/assets/javascripts/repo/components/repo_tab.vue
@@ -10,10 +10,16 @@ const RepoTab = {
},
computed: {
+ closeLabel() {
+ if (this.tab.changed) {
+ return `${this.tab.name} changed`;
+ }
+ return `Close ${this.tab.name}`;
+ },
changedClass() {
const tabChangedObj = {
- 'fa-times': !this.tab.changed,
- 'fa-circle': this.tab.changed,
+ 'fa-times close-icon': !this.tab.changed,
+ 'fa-circle unsaved-icon': this.tab.changed,
};
return tabChangedObj;
},
@@ -22,9 +28,9 @@ const RepoTab = {
methods: {
tabClicked: Store.setActiveFiles,
- xClicked(file) {
+ closeTab(file) {
if (file.changed) return;
- this.$emit('xclicked', file);
+ this.$emit('tabclosed', file);
},
},
};
@@ -33,13 +39,25 @@ export default RepoTab;
</script>
<template>
-<li>
- <a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading">
- <i class="fa" :class="changedClass"></i>
+<li @click="tabClicked(tab)">
+ <a
+ href="#0"
+ class="close"
+ @click.stop.prevent="closeTab(tab)"
+ :aria-label="closeLabel">
+ <i
+ class="fa"
+ :class="changedClass"
+ aria-hidden="true">
+ </i>
</a>
- <a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a>
-
- <i v-if="tab.loading" class="fa fa-spinner fa-spin"></i>
+ <a
+ href="#"
+ class="repo-tab"
+ :title="tab.url"
+ @click.prevent="tabClicked(tab)">
+ {{tab.name}}
+ </a>
</li>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue
index 907a03e1601..9c5bfc5d0cf 100644
--- a/app/assets/javascripts/repo/components/repo_tabs.vue
+++ b/app/assets/javascripts/repo/components/repo_tabs.vue
@@ -1,5 +1,4 @@
<script>
-import Vue from 'vue';
import Store from '../stores/repo_store';
import RepoTab from './repo_tab.vue';
import RepoMixin from '../mixins/repo_mixin';
@@ -14,30 +13,24 @@ const RepoTabs = {
data: () => Store,
methods: {
- isOverflow() {
- return this.$el.scrollWidth > this.$el.offsetWidth;
- },
-
- xClicked(file) {
+ tabClosed(file) {
Store.removeFromOpenedFiles(file);
},
},
-
- watch: {
- openedFiles() {
- Vue.nextTick(() => {
- this.tabsOverflow = this.isOverflow();
- });
- },
- },
};
export default RepoTabs;
</script>
<template>
-<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}">
- <repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/>
+<ul id="tabs">
+ <repo-tab
+ v-for="tab in openedFiles"
+ :key="tab.id"
+ :tab="tab"
+ :class="{'active' : tab.active}"
+ @tabclosed="tabClosed"
+ />
<li class="tabs-divider" />
</ul>
</template>
diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
index 8ee2df5c879..f8729bbf585 100644
--- a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
+++ b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
@@ -1,16 +1,20 @@
/* global monaco */
import RepoEditor from '../components/repo_editor.vue';
import Store from '../stores/repo_store';
+import Helper from '../helpers/repo_helper';
import monacoLoader from '../monaco_loader';
function repoEditorLoader() {
Store.monacoLoading = true;
return new Promise((resolve, reject) => {
monacoLoader(['vs/editor/editor.main'], () => {
- Store.monaco = monaco;
+ Helper.monaco = monaco;
Store.monacoLoading = false;
resolve(RepoEditor);
- }, reject);
+ }, () => {
+ Store.monacoLoading = false;
+ reject();
+ });
});
}
diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js
index fee98c12592..2bd8d7eea65 100644
--- a/app/assets/javascripts/repo/helpers/repo_helper.js
+++ b/app/assets/javascripts/repo/helpers/repo_helper.js
@@ -4,6 +4,8 @@ import Store from '../stores/repo_store';
import '../../flash';
const RepoHelper = {
+ monacoInstance: null,
+
getDefaultActiveFile() {
return {
active: true,
@@ -33,19 +35,23 @@ const RepoHelper = {
? window.performance
: Date,
- getBranch() {
- return $('button.dropdown-menu-toggle').attr('data-ref');
+ getFileExtension(fileName) {
+ return fileName.split('.').pop();
},
getLanguageIDForFile(file, langs) {
- const ext = file.name.split('.').pop();
+ const ext = RepoHelper.getFileExtension(file.name);
const foundLang = RepoHelper.findLanguage(ext, langs);
return foundLang ? foundLang.id : 'plaintext';
},
- getFilePathFromFullPath(fullPath, branch) {
- return fullPath.split(`${Store.projectUrl}/blob/${branch}`)[1];
+ setMonacoModelFromLanguage() {
+ RepoHelper.monacoInstance.setModel(null);
+ const languages = RepoHelper.monaco.languages.getLanguages();
+ const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages);
+ const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID);
+ RepoHelper.monacoInstance.setModel(newModel);
},
findLanguage(ext, langs) {
@@ -58,11 +64,11 @@ const RepoHelper = {
file.opened = true;
file.icon = 'fa-folder-open';
- RepoHelper.toURL(file.url, file.name);
+ RepoHelper.updateHistoryEntry(file.url, file.name);
return file;
},
- isKindaBinary() {
+ isRenderable() {
const okExts = ['md', 'svg'];
return okExts.indexOf(Store.activeFile.extension) > -1;
},
@@ -76,22 +82,8 @@ const RepoHelper = {
.catch(RepoHelper.loadingError);
},
- toggleFakeTab(loading, file) {
- if (loading) return Store.addPlaceholderFile();
- return Store.removeFromOpenedFiles(file);
- },
-
- setLoading(loading, file) {
- if (Service.url.indexOf('blob') > -1) {
- Store.loading.blob = loading;
- return RepoHelper.toggleFakeTab(loading, file);
- }
-
- if (Service.url.indexOf('tree') > -1) Store.loading.tree = loading;
-
- return undefined;
- },
-
+ // when you open a directory you need to put the directory files under
+ // the directory... This will merge the list of the current directory and the new list.
getNewMergedList(inDirectory, currentList, newList) {
const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
if (!inDirectory) return newListSorted;
@@ -100,6 +92,9 @@ const RepoHelper = {
return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile);
},
+ // within the get new merged list this does the merging of the current list of files
+ // and the new list of files. The files are never "in" another directory they just
+ // appear like they are because of the margin.
mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
newList.reverse().forEach((newFile) => {
const fileIndex = indexOfFile + 1;
@@ -135,21 +130,17 @@ const RepoHelper = {
return isRoot;
},
- getContent(treeOrFile, cb) {
+ getContent(treeOrFile) {
let file = treeOrFile;
- // const loadingData = RepoHelper.setLoading(true);
return Service.getContent()
.then((response) => {
const data = response.data;
- // RepoHelper.setLoading(false, loadingData);
- if (cb) cb();
Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) {
if (!file) file = data;
Store.binary = data.binary;
if (data.binary) {
- Store.binaryMimeType = data.mime_type;
// file might be undefined
RepoHelper.setBinaryDataAsBase64(data);
Store.setViewToPreview();
@@ -188,9 +179,8 @@ const RepoHelper = {
setFile(data, file) {
const newFile = data;
- newFile.url = file.url || location.pathname;
newFile.url = file.url;
- if (newFile.render_error === 'too_large') {
+ if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') {
newFile.tooLarge = true;
}
newFile.newContent = '';
@@ -199,10 +189,6 @@ const RepoHelper = {
Store.setActiveFiles(newFile);
},
- toFA(icon) {
- return `fa-${icon}`;
- },
-
serializeBlob(blob) {
const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
simpleBlob.lastCommitMessage = blob.last_commit.message;
@@ -226,7 +212,7 @@ const RepoHelper = {
type,
name,
url,
- icon: RepoHelper.toFA(icon),
+ icon: `fa-${icon}`,
level: 0,
loading: false,
};
@@ -244,42 +230,24 @@ const RepoHelper = {
setTimeout(() => {
const tabs = document.getElementById('tabs');
if (!tabs) return;
- tabs.scrollLeft = 12000;
+ tabs.scrollLeft = tabs.scrollWidth;
}, 200);
},
dataToListOfFiles(data) {
- const a = [];
-
- // push in blobs
- data.blobs.forEach((blob) => {
- a.push(RepoHelper.serializeBlob(blob));
- });
-
- data.trees.forEach((tree) => {
- a.push(RepoHelper.serializeTree(tree));
- });
-
- data.submodules.forEach((submodule) => {
- a.push(RepoHelper.serializeSubmodule(submodule));
- });
-
- return a;
+ const { blobs, trees, submodules } = data;
+ return [
+ ...blobs.map(blob => RepoHelper.serializeBlob(blob)),
+ ...trees.map(tree => RepoHelper.serializeTree(tree)),
+ ...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)),
+ ];
},
genKey() {
return RepoHelper.Time.now().toFixed(3);
},
- getStateKey() {
- return RepoHelper.key;
- },
-
- setStateKey(key) {
- RepoHelper.key = key;
- },
-
- toURL(url, title) {
+ updateHistoryEntry(url, title) {
const history = window.history;
RepoHelper.key = RepoHelper.genKey();
@@ -296,7 +264,7 @@ const RepoHelper = {
},
loadingError() {
- Flash('Unable to load the file at this time.');
+ 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 67c03680fca..6c1d468e937 100644
--- a/app/assets/javascripts/repo/index.js
+++ b/app/assets/javascripts/repo/index.js
@@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue';
import Translate from '../vue_shared/translate';
function initDropdowns() {
- $('.project-refs-target-form').hide();
- $('.fa-long-arrow-right').hide();
+ $('.js-tree-ref-target-holder').hide();
}
function addEventsForNonVueEls() {
@@ -34,6 +33,8 @@ function setInitialStore(data) {
Store.projectId = data.projectId;
Store.projectName = data.projectName;
Store.projectUrl = data.projectUrl;
+ Store.canCommit = data.canCommit;
+ Store.onTopOfBranch = data.onTopOfBranch;
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable();
}
@@ -44,6 +45,9 @@ function initRepo(el) {
components: {
repo: Repo,
},
+ render(createElement) {
+ return createElement('repo');
+ },
});
}
diff --git a/app/assets/javascripts/repo/monaco_loader.js b/app/assets/javascripts/repo/monaco_loader.js
index ad1370a7730..af83a1ec0b4 100644
--- a/app/assets/javascripts/repo/monaco_loader.js
+++ b/app/assets/javascripts/repo/monaco_loader.js
@@ -1,13 +1,11 @@
-/* eslint-disable no-underscore-dangle, camelcase */
-/* global __webpack_public_path__ */
-
import monacoContext from 'monaco-editor/dev/vs/loader';
monacoContext.require.config({
paths: {
- vs: `${__webpack_public_path__}monaco-editor/vs`,
+ vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
},
});
+// eslint-disable-next-line no-underscore-dangle
window.__monaco_context__ = monacoContext;
export default monacoContext.require;
diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js
index 8fba928e456..af83497fa39 100644
--- a/app/assets/javascripts/repo/services/repo_service.js
+++ b/app/assets/javascripts/repo/services/repo_service.js
@@ -2,6 +2,7 @@
import axios from 'axios';
import Store from '../stores/repo_store';
import Api from '../../api';
+import Helper from '../helpers/repo_helper';
const RepoService = {
url: '',
@@ -12,16 +13,9 @@ const RepoService = {
},
richExtensionRegExp: /md/,
- checkCurrentBranchIsCommitable() {
- const url = Store.service.refsUrl;
- return axios.get(url, { params: {
- ref: Store.currentBranch,
- search: Store.currentBranch,
- } });
- },
-
getRaw(url) {
return axios.get(url, {
+ // Stop Axios from parsing a JSON file into a JS object
transformResponse: [res => res],
});
},
@@ -36,7 +30,7 @@ const RepoService = {
},
urlIsRichBlob(url = this.url) {
- const extension = url.split('.').pop();
+ const extension = Helper.getFileExtension(url);
return this.richExtensionRegExp.test(extension);
},
@@ -71,11 +65,17 @@ const RepoService = {
return urlArray.join('/');
},
- commitFiles(payload, cb) {
- Api.commitMultiple(Store.projectId, payload, (data) => {
- Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
- cb();
- });
+ commitFiles(payload) {
+ return Api.commitMultiple(Store.projectId, payload)
+ .then(this.commitFlash);
+ },
+
+ commitFlash(data) {
+ if (data.short_id && data.stats) {
+ window.Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
+ } else {
+ window.Flash(data.message);
+ }
},
};
diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js
index 06ca391ed0c..1c0df528aea 100644
--- a/app/assets/javascripts/repo/stores/repo_store.js
+++ b/app/assets/javascripts/repo/stores/repo_store.js
@@ -3,13 +3,11 @@ import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
const RepoStore = {
- ideEl: {},
monaco: {},
monacoLoading: false,
- monacoInstance: {},
service: '',
- editor: '',
- sidebar: '',
+ canCommit: false,
+ onTopOfBranch: false,
editMode: false,
isTree: false,
isRoot: false,
@@ -17,19 +15,10 @@ const RepoStore = {
projectId: '',
projectName: '',
projectUrl: '',
- trees: [],
- blobs: [],
- submodules: [],
blobRaw: '',
- blobRendered: '',
currentBlobView: 'repo-preview',
openedFiles: [],
- tabSize: 100,
- defaultTabSize: 100,
- minTabSize: 30,
- tabsOverflow: 41,
submitCommitsLoading: false,
- binaryLoaded: false,
dialog: {
open: false,
title: '',
@@ -45,9 +34,6 @@ const RepoStore = {
currentBranch: '',
targetBranch: 'new-branch',
commitMessage: '',
- binaryMimeType: '',
- // scroll bar space for windows
- scrollWidth: 0,
binaryTypes: {
png: false,
md: false,
@@ -58,7 +44,6 @@ const RepoStore = {
tree: false,
blob: false,
},
- readOnly: true,
resetBinaryTypes() {
Object.keys(RepoStore.binaryTypes).forEach((key) => {
@@ -68,14 +53,7 @@ const RepoStore = {
// mutations
checkIsCommitable() {
- RepoStore.service.checkCurrentBranchIsCommitable()
- .then((data) => {
- // you shouldn't be able to make commits on commits or tags.
- const { Branches, Commits, Tags } = data.data;
- if (Branches && Branches.length) RepoStore.isCommitable = true;
- if (Commits && Commits.length) RepoStore.isCommitable = false;
- if (Tags && Tags.length) RepoStore.isCommitable = false;
- }).catch(() => Flash('Failed to check if branch can be committed to.'));
+ RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
},
addFilesToDirectory(inDirectory, currentList, newList) {
@@ -96,7 +74,6 @@ const RepoStore = {
if (file.binary) {
RepoStore.blobRaw = file.base64;
- RepoStore.binaryMimeType = file.mime_type;
} else if (file.newContent || file.plain) {
RepoStore.blobRaw = file.newContent || file.plain;
} else {
@@ -107,7 +84,7 @@ const RepoStore = {
}).catch(Helper.loadingError);
}
- if (!file.loading) Helper.toURL(file.url, file.name);
+ if (!file.loading) Helper.updateHistoryEntry(file.url, file.name);
RepoStore.binary = file.binary;
},
@@ -134,15 +111,15 @@ const RepoStore = {
removeChildFilesOfTree(tree) {
let foundTree = false;
const treeToClose = tree;
- let wereDone = false;
+ let canStopSearching = false;
RepoStore.files = RepoStore.files.filter((file) => {
const isItTheTreeWeWant = file.url === treeToClose.url;
// if it's the next tree
if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
- wereDone = true;
+ canStopSearching = true;
return true;
}
- if (wereDone) return true;
+ if (canStopSearching) return true;
if (isItTheTreeWeWant) foundTree = true;
@@ -159,8 +136,8 @@ const RepoStore = {
if (file.type === 'tree') return;
let foundIndex;
RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
- if (openedFile.url === file.url) foundIndex = i;
- return openedFile.url !== file.url;
+ if (openedFile.path === file.path) foundIndex = i;
+ return openedFile.path !== file.path;
});
// now activate the right tab based on what you closed.
@@ -174,36 +151,16 @@ const RepoStore = {
return;
}
- if (foundIndex) {
- if (foundIndex > 0) {
- RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
- }
+ if (foundIndex && foundIndex > 0) {
+ RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
}
},
- addPlaceholderFile() {
- const randomURL = Helper.Time.now();
- const newFakeFile = {
- active: false,
- binary: true,
- type: 'blob',
- loading: true,
- mime_type: 'loading',
- name: 'loading',
- url: randomURL,
- fake: true,
- };
-
- RepoStore.openedFiles.push(newFakeFile);
-
- return newFakeFile;
- },
-
addToOpenedFiles(file) {
const openFile = file;
const openedFilesAlreadyExists = RepoStore.openedFiles
- .some(openedFile => openedFile.url === openFile.url);
+ .some(openedFile => openedFile.path === openFile.path);
if (openedFilesAlreadyExists) return;
@@ -238,4 +195,5 @@ const RepoStore = {
return RepoStore.currentBlobView === 'repo-preview';
},
};
+
export default RepoStore;
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index fa958d75fa4..a4eae135403 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -2,7 +2,6 @@
import _ from 'underscore';
import Cookies from 'js-cookie';
-import SidebarHeightManager from './sidebar_height_manager';
(function() {
this.Sidebar = (function() {
@@ -23,7 +22,6 @@ import SidebarHeightManager from './sidebar_height_manager';
};
Sidebar.prototype.addEventListeners = function() {
- SidebarHeightManager.init();
const $document = $(document);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
@@ -157,11 +155,16 @@ import SidebarHeightManager from './sidebar_height_manager';
Sidebar.prototype.openDropdown = function(blockOrName) {
var $block;
$block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
- $block.find('.edit-link').trigger('click');
if (!this.isOpen()) {
this.setCollapseAfterUpdate($block);
- return this.toggleSidebar('open');
+ this.toggleSidebar('open');
}
+
+ // Wait for the sidebar to trigger('click') open
+ // so it doesn't cause our dropdown to close preemptively
+ setTimeout(() => {
+ $block.find('.js-sidebar-dropdown-toggle').trigger('click');
+ });
};
Sidebar.prototype.setCollapseAfterUpdate = function($block) {
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 6fd5345a0a6..003a15592f3 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -363,7 +363,7 @@
restoreMenu() {
var html;
- html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
+ html = '<ul><li class="dropdown-menu-empty-item"><a>Loading...</a></li></ul>';
return this.dropdownContent.html(html);
}
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index 7fa5996d600..8635ccece6e 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -41,4 +41,8 @@ export default function initSettingsPanels() {
$section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
$section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section));
});
+
+ if (location.hash) {
+ expandSection($(location.hash));
+ }
}
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 0be141eb5f9..78b257bf192 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -20,7 +20,7 @@ import './shortcuts_navigation';
Mousetrap.bind('m', this.openSidebarDropdown.bind(this, 'milestone'));
Mousetrap.bind('r', (function(_this) {
return function() {
- _this.replyWithSelectedText();
+ _this.replyWithSelectedText(isMergeRequest);
return false;
};
})(this));
@@ -38,9 +38,15 @@ import './shortcuts_navigation';
}
}
- ShortcutsIssuable.prototype.replyWithSelectedText = function() {
+ ShortcutsIssuable.prototype.replyWithSelectedText = function(isMergeRequest) {
var quote, documentFragment, el, selected, separator;
- var replyField = $('.js-main-target-form #note_note');
+ let replyField;
+
+ if (isMergeRequest) {
+ replyField = $('.js-main-target-form #note_note');
+ } else {
+ replyField = $('.js-main-target-form .js-vue-comment-form');
+ }
documentFragment = window.gl.utils.getSelectedFragment();
if (!documentFragment) {
@@ -57,6 +63,7 @@ import './shortcuts_navigation';
quote = _.map(selected.split("\n"), function(val) {
return ("> " + val).trim() + "\n";
});
+
// If replyField already has some content, add a newline before our quote
separator = replyField.val().trim() !== "" && "\n\n" || '';
replyField.val(function(a, current) {
@@ -64,7 +71,7 @@ import './shortcuts_navigation';
});
// Trigger autosave
- replyField.trigger('input');
+ replyField.trigger('input').trigger('change');
// Trigger autosize
var event = document.createEvent('Event');
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js
index 5a6e47e566e..77f070d48cc 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js
@@ -36,7 +36,7 @@ export default {
/>
<a
v-if="editable"
- class="edit-link pull-right"
+ class="js-sidebar-dropdown-toggle edit-link pull-right"
href="#"
>
Edit
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 422c02c7b7e..8e7abdbffef 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -71,7 +71,7 @@ export default {
/>
<div v-if="!isConfidential" class="no-value confidential-value">
<i class="fa fa-eye is-not-confidential"></i>
- None
+ 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>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js
index 2d682215cf8..d32fe4abc7d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.js
@@ -6,6 +6,7 @@ import timeTracker from './time_tracker';
import Store from '../../stores/sidebar_store';
import Mediator from '../../sidebar_mediator';
+import eventHub from '../../event_hub';
export default {
data() {
@@ -20,6 +21,9 @@ export default {
methods: {
listenForQuickActions() {
$(document).on('ajax:success', '.gfm-form', this.quickActionListened);
+ eventHub.$on('timeTrackingUpdated', (data) => {
+ this.quickActionListened(null, data);
+ });
},
quickActionListened(e, data) {
const subscribedCommands = ['spend_time', 'time_estimate'];
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
new file mode 100644
index 00000000000..1c15a1b877a
--- /dev/null
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -0,0 +1,85 @@
+/* global Flash */
+
+function isValidProjectId(id) {
+ return id > 0;
+}
+
+class SidebarMoveIssue {
+ constructor(mediator, dropdownToggle, confirmButton) {
+ this.mediator = mediator;
+
+ this.$dropdownToggle = $(dropdownToggle);
+ this.$confirmButton = $(confirmButton);
+
+ this.onConfirmClickedWrapper = this.onConfirmClicked.bind(this);
+ }
+
+ init() {
+ this.initDropdown();
+ this.addEventListeners();
+ }
+
+ destroy() {
+ this.removeEventListeners();
+ }
+
+ initDropdown() {
+ this.$dropdownToggle.glDropdown({
+ search: {
+ fields: ['name_with_namespace'],
+ },
+ showMenuAbove: true,
+ selectable: true,
+ filterable: true,
+ filterRemote: true,
+ multiSelect: false,
+ // Keep the dropdown open after selecting an option
+ shouldPropagate: false,
+ data: (searchTerm, callback) => {
+ this.mediator.fetchAutocompleteProjects(searchTerm)
+ .then(callback)
+ .catch(() => new Flash('An error occured while fetching projects autocomplete.'));
+ },
+ renderRow: project => `
+ <li>
+ <a href="#" class="js-move-issue-dropdown-item">
+ ${project.name_with_namespace}
+ </a>
+ </li>
+ `,
+ clicked: (options) => {
+ const project = options.selectedObj;
+ const selectedProjectId = options.isMarking ? project.id : 0;
+ this.mediator.setMoveToProjectId(selectedProjectId);
+
+ this.$confirmButton.attr('disabled', !isValidProjectId(selectedProjectId));
+ },
+ });
+ }
+
+ addEventListeners() {
+ this.$confirmButton.on('click', this.onConfirmClickedWrapper);
+ }
+
+ removeEventListeners() {
+ this.$confirmButton.off('click', this.onConfirmClickedWrapper);
+ }
+
+ onConfirmClicked() {
+ if (isValidProjectId(this.mediator.store.moveToProjectId)) {
+ this.$confirmButton
+ .disable()
+ .addClass('is-loading');
+
+ this.mediator.moveIssue()
+ .catch(() => {
+ Flash('An error occured while moving the issue.');
+ this.$confirmButton
+ .enable()
+ .removeClass('is-loading');
+ });
+ }
+ }
+}
+
+export default SidebarMoveIssue;
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 5a82d01dc41..604648407a4 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -4,9 +4,11 @@ import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class SidebarService {
- constructor(endpoint) {
+ constructor(endpointMap) {
if (!SidebarService.singleton) {
- this.endpoint = endpoint;
+ this.endpoint = endpointMap.endpoint;
+ this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
+ this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
SidebarService.singleton = this;
}
@@ -25,4 +27,18 @@ export default class SidebarService {
emulateJSON: true,
});
}
+
+ getProjectsAutocomplete(searchTerm) {
+ return Vue.http.get(this.projectsAutocompleteEndpoint, {
+ params: {
+ search: searchTerm,
+ },
+ });
+ }
+
+ moveIssue(moveToProjectId) {
+ return Vue.http.post(this.moveIssueEndpoint, {
+ move_to_project_id: moveToProjectId,
+ });
+ }
}
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 9edded3ead6..3d8972050a9 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -2,6 +2,7 @@ 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 SidebarMoveIssue from './lib/sidebar_move_issue';
import Mediator from './sidebar_mediator';
@@ -31,6 +32,12 @@ function domContentLoaded() {
service: mediator.service,
},
}).$mount(confidentialEl);
+
+ new SidebarMoveIssue(
+ mediator,
+ $('.js-move-issue'),
+ $('.js-move-issue-confirmation-button'),
+ ).init();
}
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 721e92221cf..e38a8db4cc5 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -7,7 +7,11 @@ export default class SidebarMediator {
constructor(options) {
if (!SidebarMediator.singleton) {
this.store = new Store(options);
- this.service = new Service(options.endpoint);
+ this.service = new Service({
+ endpoint: options.endpoint,
+ moveIssueEndpoint: options.moveIssueEndpoint,
+ projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
+ });
SidebarMediator.singleton = this;
}
@@ -26,6 +30,10 @@ export default class SidebarMediator {
return this.service.update(field, selected.length === 0 ? [0] : selected);
}
+ setMoveToProjectId(projectId) {
+ this.store.setMoveToProjectId(projectId);
+ }
+
fetch() {
this.service.get()
.then(response => response.json())
@@ -35,4 +43,23 @@ export default class SidebarMediator {
})
.catch(() => new Flash('Error occured when fetching sidebar data'));
}
+
+ fetchAutocompleteProjects(searchTerm) {
+ return this.service.getProjectsAutocomplete(searchTerm)
+ .then(response => response.json())
+ .then((data) => {
+ this.store.setAutocompleteProjects(data);
+ return this.store.autocompleteProjects;
+ });
+ }
+
+ moveIssue() {
+ return this.service.moveIssue(this.store.moveToProjectId)
+ .then(response => response.json())
+ .then((data) => {
+ if (location.pathname !== data.web_url) {
+ gl.utils.visitUrl(data.web_url);
+ }
+ });
+ }
}
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 3356dd0191f..cc04a2a3fcf 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -13,6 +13,8 @@ export default class SidebarStore {
this.isFetching = {
assignees: true,
};
+ this.autocompleteProjects = [];
+ this.moveToProjectId = 0;
SidebarStore.singleton = this;
}
@@ -53,4 +55,12 @@ export default class SidebarStore {
removeAllAssignees() {
this.assignees = [];
}
+
+ setAutocompleteProjects(projects) {
+ this.autocompleteProjects = projects;
+ }
+
+ setMoveToProjectId(moveToProjectId) {
+ this.moveToProjectId = moveToProjectId;
+ }
}
diff --git a/app/assets/javascripts/sidebar_height_manager.js b/app/assets/javascripts/sidebar_height_manager.js
deleted file mode 100644
index df19d7305f8..00000000000
--- a/app/assets/javascripts/sidebar_height_manager.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import _ from 'underscore';
-
-export default {
- init() {
- if (!this.initialized) {
- this.$window = $(window);
- this.$rightSidebar = $('.js-right-sidebar');
- this.$navHeight = $('.navbar-gitlab').outerHeight() +
- $('.layout-nav').outerHeight() +
- $('.sub-nav-scroll').outerHeight();
-
- const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20);
- const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200);
-
- this.$window.on('scroll', throttledSetSidebarHeight);
- this.$window.on('resize', debouncedSetSidebarHeight);
- this.initialized = true;
- }
- },
-
- setSidebarHeight() {
- const currentScrollDepth = window.pageYOffset || 0;
- const diff = this.$navHeight - currentScrollDepth;
-
- if (diff > 0) {
- const newSidebarHeight = window.innerHeight - diff;
- this.$rightSidebar.outerHeight(newSidebarHeight);
- this.sidebarHeightIsCustom = true;
- } else if (this.sidebarHeightIsCustom) {
- this.$rightSidebar.outerHeight('100%');
- this.sidebarHeightIsCustom = false;
- }
- },
-};
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
index ff2208baeab..a45b22f3084 100644
--- a/app/assets/javascripts/user_callout.js
+++ b/app/assets/javascripts/user_callout.js
@@ -1,7 +1,11 @@
import Cookies from 'js-cookie';
export default class UserCallout {
- constructor(className = 'user-callout') {
+ constructor(options = {}) {
+ this.options = options;
+
+ const className = this.options.className || 'user-callout';
+
this.userCalloutBody = $(`.${className}`);
this.cookieName = this.userCalloutBody.data('uid');
this.isCalloutDismissed = Cookies.get(this.cookieName);
@@ -17,7 +21,11 @@ export default class UserCallout {
dismissCallout(e) {
const $currentTarget = $(e.currentTarget);
- Cookies.set(this.cookieName, 'true', { expires: 365 });
+ if (this.options.setCalloutPerProject) {
+ Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('project-path') });
+ } else {
+ Cookies.set(this.cookieName, 'true', { expires: 365 });
+ }
if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove();
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 16ebf5916dc..a31fedee021 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -588,9 +588,10 @@ function UsersSelect(currentUser, els) {
if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
var trimmed = query.term.trim();
emailUser = {
- name: "Invite \"" + query.term + "\"",
+ name: "Invite \"" + query.term + "\" by email",
username: trimmed,
- id: trimmed
+ id: trimmed,
+ invite: true
};
data.results.unshift(emailUser);
}
@@ -642,7 +643,7 @@ 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.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'>" + user.name + "</div> <div class='user-username dropdown-menu-user-username'>" + (!user.invite ? "@" + _.escape(user.username) : "") + "</div> </div>";
};
UsersSelect.prototype.formatSelection = function(user) {
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 c05a76a3b4a..aaca42e3ebc 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
@@ -75,18 +75,20 @@ export default {
class="btn btn-small inline">
Check out branch
</a>
- <span class="dropdown inline prepend-left-10">
+ <span class="dropdown prepend-left-10">
<a
- class="btn btn-xs dropdown-toggle"
+ class="btn btn-small inline dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
role="button">
<i
class="fa fa-download"
- aria-hidden="true" />
+ aria-hidden="true">
+ </i>
<i
class="fa fa-caret-down"
- aria-hidden="true" />
+ aria-hidden="true">
+ </i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li>
diff --git a/app/assets/javascripts/vue_shared/common_vue.js b/app/assets/javascripts/vue_shared/common_vue.js
deleted file mode 100644
index eb2a6071fda..00000000000
--- a/app/assets/javascripts/vue_shared/common_vue.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import Vue from 'vue';
-import './vue_resource_interceptor';
-
-if (process.env.NODE_ENV !== 'production') {
- Vue.config.productionTip = false;
-}
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 262584769e0..50d14282cad 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -1,6 +1,7 @@
<script>
import commitIconSvg from 'icons/_icon_commit.svg';
import userAvatarLink from './user_avatar/user_avatar_link.vue';
+ import tooltip from '../directives/tooltip';
export default {
props: {
@@ -100,17 +101,22 @@
this.author.username ? `${this.author.username}'s avatar` : null;
},
},
- data() {
- return { commitIconSvg };
+ directives: {
+ tooltip,
},
components: {
userAvatarLink,
},
+ created() {
+ this.commitIconSvg = commitIconSvg;
+ },
};
</script>
<template>
<div class="branch-commit">
- <div v-if="hasCommitRef" class="icon-container hidden-xs">
+ <div
+ v-if="hasCommitRef"
+ class="icon-container hidden-xs">
<i
v-if="tag"
class="fa fa-tag"
@@ -126,7 +132,10 @@
<a
v-if="hasCommitRef"
class="ref-name hidden-xs"
- :href="commitRef.ref_url">
+ :href="commitRef.ref_url"
+ v-tooltip
+ data-container="body"
+ :title="commitRef.name">
{{commitRef.name}}
</a>
@@ -153,7 +162,8 @@
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
/>
- <a class="commit-row-message"
+ <a
+ class="commit-row-message"
:href="commitUrl">
{{title}}
</a>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index bdc059f4a03..d305bd6acdc 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -120,7 +120,7 @@ export default {
</a>
<a
- v-if="action.type === 'ujs-link'"
+ v-else-if="action.type === 'ujs-link'"
:href="action.path"
data-method="post"
rel="nofollow"
@@ -129,7 +129,7 @@ export default {
</a>
<button
- v-else="action.type === 'button'"
+ v-else-if="action.type === 'button'"
@click="onClickAction(action)"
:disabled="action.isLoading"
:class="action.cssClass"
diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue
new file mode 100644
index 00000000000..7cf2e029cf6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/identicon.vue
@@ -0,0 +1,51 @@
+<script>
+export default {
+ props: {
+ entityId: {
+ type: Number,
+ required: true,
+ },
+ entityName: {
+ type: String,
+ required: true,
+ },
+ sizeClass: {
+ type: String,
+ required: false,
+ default: 's40',
+ },
+ },
+ computed: {
+ /**
+ * This method is based on app/helpers/application_helper.rb#project_identicon
+ */
+ identiconStyles() {
+ const allowedColors = [
+ '#FFEBEE',
+ '#F3E5F5',
+ '#E8EAF6',
+ '#E3F2FD',
+ '#E0F2F1',
+ '#FBE9E7',
+ '#EEEEEE',
+ ];
+
+ const backgroundColor = allowedColors[this.entityId % 7];
+
+ return `background-color: ${backgroundColor}; color: #555;`;
+ },
+ identiconTitle() {
+ return this.entityName.charAt(0).toUpperCase();
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="avatar identicon"
+ :class="sizeClass"
+ :style="identiconStyles">
+ {{identiconTitle}}
+ </div>
+</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
new file mode 100644
index 00000000000..397d16331d5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue
@@ -0,0 +1,16 @@
+<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/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 4e10bbc7408..759d30c9c7c 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -5,19 +5,30 @@
export default {
props: {
- markdownPreviewUrl: {
+ markdownPreviewPath: {
type: String,
required: false,
default: '',
},
- markdownDocs: {
+ markdownDocsPath: {
type: String,
required: true,
},
+ addSpacingClasses: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ quickActionsDocsPath: {
+ type: String,
+ required: false,
+ },
},
data() {
return {
markdownPreview: '',
+ referencedCommands: '',
+ referencedUsers: '',
markdownPreviewLoading: false,
previewMarkdown: false,
};
@@ -26,35 +37,48 @@
markdownHeader,
markdownToolbar,
},
+ computed: {
+ shouldShowReferencedUsers() {
+ const referencedUsersThreshold = 10;
+ return this.referencedUsers.length >= referencedUsersThreshold;
+ },
+ },
methods: {
toggleMarkdownPreview() {
this.previewMarkdown = !this.previewMarkdown;
+ /*
+ Can't use `$refs` as the component is technically in the parent component
+ so we access the VNode & then get the element
+ */
+ const text = this.$slots.textarea[0].elm.value;
+
if (!this.previewMarkdown) {
this.markdownPreview = '';
- } else {
+ } else if (text) {
this.markdownPreviewLoading = true;
- this.$http.post(
- this.markdownPreviewUrl,
- {
- /*
- Can't use `$refs` as the component is technically in the parent component
- so we access the VNode & then get the element
- */
- text: this.$slots.textarea[0].elm.value,
- },
- )
- .then(resp => resp.json())
- .then((data) => {
- this.markdownPreviewLoading = false;
- this.markdownPreview = data.body;
+ this.$http.post(this.markdownPreviewPath, { text })
+ .then(resp => resp.json())
+ .then((data) => {
+ this.renderMarkdown(data);
+ })
+ .catch(() => new Flash('Error loading markdown preview'));
+ } else {
+ this.renderMarkdown();
+ }
+ },
+ renderMarkdown(data = {}) {
+ this.markdownPreviewLoading = false;
+ this.markdownPreview = data.body || 'Nothing to preview.';
- this.$nextTick(() => {
- $(this.$refs['markdown-preview']).renderGFM();
- });
- })
- .catch(() => new Flash('Error loading markdown preview'));
+ if (data.references) {
+ this.referencedCommands = data.references.commands;
+ this.referencedUsers = data.references.users;
}
+
+ this.$nextTick(() => {
+ $(this.$refs['markdown-preview']).renderGFM();
+ });
},
},
mounted() {
@@ -74,7 +98,8 @@
<template>
<div
- class="md-area prepend-top-default append-bottom-default js-vue-markdown-field"
+ class="md-area js-vue-markdown-field"
+ :class="{ 'prepend-top-default append-bottom-default': addSpacingClasses }"
ref="gl-form">
<markdown-header
:preview-markdown="previewMarkdown"
@@ -94,7 +119,9 @@
</i>
</a>
<markdown-toolbar
- :markdown-docs="markdownDocs" />
+ :markdown-docs-path="markdownDocsPath"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ />
</div>
</div>
<div
@@ -108,5 +135,27 @@
Loading...
</span>
</div>
+ <template v-if="previewMarkdown && !markdownPreviewLoading">
+ <div
+ v-if="referencedCommands"
+ v-html="referencedCommands"
+ class="referenced-commands"></div>
+ <div
+ v-if="shouldShowReferencedUsers"
+ class="referenced-users">
+ <span>
+ <i
+ class="fa fa-exclamation-triangle"
+ aria-hidden="true">
+ </i>
+ You are about to add
+ <strong>
+ <span class="js-referenced-users-count">
+ {{referencedUsers.length}}
+ </span>
+ </strong> people to the discussion. Proceed with caution.
+ </span>
+ </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 93252293ba6..65fe7bbd94e 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -1,10 +1,14 @@
<script>
export default {
props: {
- markdownDocs: {
+ markdownDocsPath: {
type: String,
required: true,
},
+ quickActionsDocsPath: {
+ type: String,
+ required: false,
+ },
},
};
</script>
@@ -12,22 +16,77 @@
<template>
<div class="comment-toolbar clearfix">
<div class="toolbar-text">
- <a
- :href="markdownDocs"
- target="_blank"
- tabindex="-1">
- Markdown is supported
- </a>
+ <template v-if="!quickActionsDocsPath && markdownDocsPath">
+ <a
+ :href="markdownDocsPath"
+ target="_blank"
+ tabindex="-1">
+ Markdown is supported
+ </a>
+ </template>
+ <template v-if="quickActionsDocsPath && markdownDocsPath">
+ <a
+ :href="markdownDocsPath"
+ target="_blank"
+ tabindex="-1">
+ Markdown
+ </a>
+ and
+ <a
+ :href="quickActionsDocsPath"
+ target="_blank"
+ tabindex="-1">
+ quick actions
+ </a>
+ are supported
+ </template>
</div>
- <button
- class="toolbar-button markdown-selector"
- type="button"
- tabindex="-1">
- <i
- class="fa fa-file-image-o toolbar-button-icon"
- aria-hidden="true">
- </i>
- Attach a file
- </button>
+ <span class="uploading-container">
+ <span class="uploading-progress-container hide">
+ <i
+ class="fa fa-file-image-o toolbar-button-icon"
+ aria-hidden="true"></i>
+ <span class="attaching-file-message"></span>
+ <span class="uploading-progress">0%</span>
+ <span class="uploading-spinner">
+ <i
+ class="fa fa-spinner fa-spin toolbar-button-icon"
+ aria-hidden="true"></i>
+ </span>
+ </span>
+ <span class="uploading-error-container hide">
+ <span class="uploading-error-icon">
+ <i
+ class="fa fa-file-image-o toolbar-button-icon"
+ aria-hidden="true"></i>
+ </span>
+ <span class="uploading-error-message"></span>
+ <button
+ class="retry-uploading-link"
+ type="button">
+ Try again
+ </button>
+ or
+ <button
+ class="attach-new-file markdown-selector"
+ type="button">
+ attach a new file
+ </button>
+ </span>
+ <button
+ class="markdown-selector button-attach-file"
+ tabindex="-1"
+ type="button">
+ <i
+ class="fa fa-file-image-o toolbar-button-icon"
+ aria-hidden="true"></i>
+ Attach a file
+ </button>
+ <button
+ class="btn btn-default btn-xs hide button-cancel-uploading-files"
+ type="button">
+ Cancel
+ </button>
+ </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 7d339c0e753..994b33bc1c9 100644
--- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue
+++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
@@ -1,31 +1,37 @@
<script>
-const PopupDialog = {
+export default {
name: 'popup-dialog',
props: {
- open: Boolean,
- title: String,
- body: String,
+ title: {
+ type: String,
+ required: true,
+ },
+ body: {
+ type: String,
+ required: true,
+ },
kind: {
type: String,
+ required: false,
default: 'primary',
},
closeButtonLabel: {
type: String,
+ required: false,
default: 'Cancel',
},
primaryButtonLabel: {
type: String,
- default: 'Save changes',
+ required: true,
},
},
computed: {
- typeOfClass() {
- const className = `btn-${this.kind}`;
- const returnObj = {};
- returnObj[className] = true;
- return returnObj;
+ btnKindClass() {
+ return {
+ [`btn-${this.kind}`]: true,
+ };
},
},
@@ -33,33 +39,45 @@ const PopupDialog = {
close() {
this.$emit('toggle', false);
},
-
- yesClick() {
- this.$emit('submit', true);
- },
-
- noClick() {
- this.$emit('submit', false);
+ emitSubmit(status) {
+ this.$emit('submit', status);
},
},
};
-
-export default PopupDialog;
</script>
+
<template>
-<div class="modal popup-dialog" tabindex="-1" v-show="open" role="dialog">
+<div
+ class="modal popup-dialog"
+ role="dialog"
+ tabindex="-1">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
- <button type="button" class="close" @click="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <button type="button"
+ class="close"
+ @click="close"
+ aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
<h4 class="modal-title">{{this.title}}</h4>
</div>
<div class="modal-body">
<p>{{this.body}}</p>
</div>
<div class="modal-footer">
- <button type="button" class="btn btn-default" data-dismiss="modal" @click="noClick">{{closeButtonLabel}}</button>
- <button type="button" class="btn" :class="typeOfClass" @click="yesClick">{{primaryButtonLabel}}</button>
+ <button
+ type="button"
+ class="btn btn-default"
+ @click="emitSubmit(false)">
+ {{closeButtonLabel}}
+ </button>
+ <button type="button"
+ class="btn"
+ :class="btnKindClass"
+ @click="emitSubmit(true)">
+ {{primaryButtonLabel}}
+ </button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/directives/popover.js b/app/assets/javascripts/vue_shared/directives/popover.js
new file mode 100644
index 00000000000..05fa563cbd0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/directives/popover.js
@@ -0,0 +1,20 @@
+/**
+ * Helper to user bootstrap popover in vue.js.
+ * Follow docs for html attributes: https://getbootstrap.com/docs/3.3/javascript/#static-popover
+ *
+ * @example
+ * import popover from 'vue_shared/directives/popover.js';
+ * {
+ * directives: [popover]
+ * }
+ * <a v-popover="{options}">popover</a>
+ */
+export default {
+ bind(el, binding) {
+ $(el).popover(binding.value);
+ },
+
+ unbind(el) {
+ $(el).popover('destroy');
+ },
+};
diff --git a/app/assets/javascripts/webpack.js b/app/assets/javascripts/webpack.js
index 9a9cf395fb8..ced847294ae 100644
--- a/app/assets/javascripts/webpack.js
+++ b/app/assets/javascripts/webpack.js
@@ -5,5 +5,5 @@
*/
if (gon && gon.webpack_public_path) {
- __webpack_public_path__ = gon.webpack_public_path; // eslint-disable-line
+ __webpack_public_path__ = gon.webpack_public_path; // eslint-disable-line camelcase
}
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
index 51ed2b4fd15..a0025ddb598 100644
--- a/app/assets/javascripts/wikis.js
+++ b/app/assets/javascripts/wikis.js
@@ -1,10 +1,7 @@
-/* global Breakpoints */
-
-import './breakpoints';
+import bp from './breakpoints';
export default class Wikis {
constructor() {
- this.bp = Breakpoints.get();
this.sidebarEl = document.querySelector('.js-wiki-sidebar');
this.sidebarExpanded = false;
@@ -41,15 +38,15 @@ export default class Wikis {
this.renderSidebar();
}
- sidebarCanCollapse() {
- const bootstrapBreakpoint = this.bp.getBreakpointSize();
+ static sidebarCanCollapse() {
+ const bootstrapBreakpoint = bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
}
renderSidebar() {
if (!this.sidebarEl) return;
const { classList } = this.sidebarEl;
- if (this.sidebarExpanded || !this.sidebarCanCollapse()) {
+ if (this.sidebarExpanded || !Wikis.sidebarCanCollapse()) {
if (!classList.contains('right-sidebar-expanded')) {
classList.remove('right-sidebar-collapsed');
classList.add('right-sidebar-expanded');
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index b2b3297e880..35e7a10379f 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -19,6 +19,7 @@
@import "framework/flash";
@import "framework/forms";
@import "framework/gfm";
+@import "framework/gitlab-theme";
@import "framework/header";
@import "framework/highlight";
@import "framework/issue_box";
@@ -51,3 +52,4 @@
@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 3cd7f81da47..667b73e150d 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -187,3 +187,81 @@ a {
.fade-in-full {
animation: fadeInFull $fade-in-duration 1;
}
+
+
+.animation-container {
+ background: $repo-editor-grey;
+ height: 40px;
+ overflow: hidden;
+ position: relative;
+
+ &.animation-container-small {
+ height: 12px;
+ }
+
+ &::before {
+ animation-duration: 1s;
+ animation-fill-mode: forwards;
+ animation-iteration-count: infinite;
+ animation-name: blockTextShine;
+ animation-timing-function: linear;
+ background-image: $repo-editor-linear-gradient;
+ background-repeat: no-repeat;
+ background-size: 800px 45px;
+ content: ' ';
+ display: block;
+ height: 100%;
+ position: relative;
+ }
+
+ div {
+ background: $white-light;
+ height: 6px;
+ left: 0;
+ position: absolute;
+ right: 0;
+ }
+
+ .skeleton-line-1 {
+ left: 0;
+ top: 8px;
+ }
+
+ .skeleton-line-2 {
+ left: 150px;
+ top: 0;
+ height: 10px;
+ }
+
+ .skeleton-line-3 {
+ left: 0;
+ top: 23px;
+ }
+
+ .skeleton-line-4 {
+ left: 0;
+ top: 38px;
+ }
+
+ .skeleton-line-5 {
+ left: 200px;
+ top: 28px;
+ height: 10px;
+ }
+
+ .skeleton-line-6 {
+ top: 14px;
+ left: 230px;
+ height: 10px;
+ }
+}
+
+@keyframes blockTextShine {
+ 0% {
+ transform: translateX(-468px);
+ }
+
+ 100% {
+ transform: translateX(468px);
+ }
+}
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 486d88efbc5..bdcbd4021b3 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -78,7 +78,7 @@
&.s60 { font-size: 32px; line-height: 58px; }
&.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; }
- &.s110 { font-size: 40px; line-height: 108px; font-weight: 300; }
+ &.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/badges.scss b/app/assets/stylesheets/framework/badges.scss
index 47a8f44c709..6bbe32df772 100644
--- a/app/assets/stylesheets/framework/badges.scss
+++ b/app/assets/stylesheets/framework/badges.scss
@@ -1,5 +1,5 @@
.badge {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
background-color: $badge-bg;
color: $badge-color;
vertical-align: baseline;
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 95a08c960ea..b575ec9de18 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -8,7 +8,7 @@
text-align: center;
padding: 20px;
color: $gl-text-color;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
font-size: 14px;
line-height: 36px;
@@ -213,7 +213,7 @@
h1 {
display: inline;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
font-size: 24px;
color: $gl-text-color;
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 6eabdc63d9e..82350c36df0 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,7 +1,7 @@
@mixin btn-default {
border-radius: 3px;
font-size: $gl-font-size;
- font-weight: 400;
+ font-weight: $gl-font-weight-normal;
padding: $gl-vert-padding $gl-btn-padding;
&:focus,
@@ -46,6 +46,15 @@
}
}
+@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;
@@ -123,6 +132,7 @@
.btn {
@include btn-default;
@include btn-white;
+ @include btn-svg;
color: $gl-text-color;
@@ -222,13 +232,6 @@
}
}
- svg {
- height: 15px;
- width: 15px;
- position: relative;
- top: 2px;
- }
-
svg,
.fa {
&:not(:last-child) {
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 0ded4a3b423..c165ec0b94b 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -52,13 +52,13 @@
.pika-label {
color: $gl-text-color-secondary;
font-size: 14px;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
th {
padding: 2px 0;
color: $note-disabled-comment-color;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
text-transform: lowercase;
border-top: 1px solid $calendar-border-color;
}
@@ -88,15 +88,15 @@
.is-today {
.pika-day {
color: inherit;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
}
.is-selected .pika-day,
.pika-day:hover,
.is-today .pika-day {
- background: $gl-primary;
- color: $white-light;
+ background: $gray-darker;
+ color: $gl-text-color;
box-shadow: none;
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 5e374360359..706a9cffe87 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -16,10 +16,12 @@
.prepend-left-default { margin-left: $gl-padding; }
.prepend-left-20 { margin-left: 20px; }
.append-right-5 { margin-right: 5px; }
+.append-right-8 { margin-right: 8px; }
.append-right-10 { margin-right: 10px; }
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
+.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; }
@@ -36,12 +38,12 @@
color: $common-gray;
font-size: 14px;
margin-bottom: 12px;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
line-height: 24px;
}
.bold {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
.tab-content {
@@ -89,7 +91,7 @@ hr {
}
}
-.item-title { font-weight: 600; }
+.item-title { font-weight: $gl-font-weight-bold; }
/** FLASH message **/
.author_link,
@@ -118,18 +120,18 @@ table a code {
span.update-author {
display: block;
color: $update-author-color;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
font-style: italic;
strong {
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
font-style: normal;
}
}
.user-mention {
color: $user-mention-color;
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
.field_with_errors {
@@ -222,7 +224,7 @@ li.note {
text-align: center;
background: $error-bg;
color: $white-light;
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
a {
color: $white-light;
@@ -339,7 +341,7 @@ table {
.header-with-avatar {
h3 {
margin: 0;
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
.username {
@@ -372,6 +374,10 @@ table {
background: $gl-success !important;
}
+.dz-message {
+ margin: 0;
+}
+
.space-right {
margin-right: 10px;
}
@@ -406,11 +412,12 @@ table {
.gl-accessibility {
&:focus {
+ display: flex;
+ align-items: center;
top: 1px;
left: 1px;
width: auto;
height: 100%;
- line-height: 50px;
padding: 0 10px;
clip: auto;
text-decoration: none;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 02e0ba74158..2bcd23a15e6 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -163,12 +163,6 @@
}
}
- &.dropdown-menu-empty-link {
- &.is-focused {
- background-color: $dropdown-empty-row-bg;
- }
- }
-
&.dropdown-menu-user-link {
line-height: 16px;
}
@@ -189,13 +183,13 @@
width: auto;
top: 100%;
left: 0;
- z-index: 9;
+ z-index: 300;
min-width: 240px;
max-width: 500px;
margin-top: 2px;
- margin-bottom: 0;
+ margin-bottom: 2px;
font-size: 14px;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
padding: 8px 0;
background-color: $white-light;
border: 1px solid $dropdown-border-color;
@@ -256,6 +250,13 @@
@include dropdown-link;
}
+ .dropdown-menu-empty-item a {
+ &:hover,
+ &:focus {
+ background-color: transparent;
+ }
+ }
+
.dropdown-header {
color: $gl-text-color-secondary;
font-size: 13px;
@@ -268,7 +269,7 @@
}
.dropdown-bold-header {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
line-height: 22px;
padding: 0 16px;
}
@@ -368,6 +369,10 @@
transform: translateY(0);
}
+.comment-type-dropdown.open .dropdown-menu {
+ display: block;
+}
+
.filtered-search-box-input-container {
.dropdown-menu,
.dropdown-menu-nav {
@@ -432,7 +437,7 @@
.dropdown-menu-user-full-name {
display: block;
- font-weight: 500;
+ font-weight: $gl-font-weight-normal;
line-height: 16px;
text-overflow: ellipsis;
overflow: hidden;
@@ -468,7 +473,7 @@
&.is-indeterminate,
&.is-active {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
color: $gl-text-color;
&::before {
@@ -502,7 +507,7 @@
position: relative;
padding: 2px 25px 10px;
margin: 0 10px 10px;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
line-height: 1;
text-align: center;
text-overflow: ellipsis;
@@ -618,6 +623,11 @@
border-top: 1px solid $dropdown-divider-color;
}
+.dropdown-footer-content {
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
.dropdown-due-date-footer {
padding-top: 0;
margin-left: 10px;
@@ -685,7 +695,7 @@
.dropdown-menu-inner-title {
display: block;
color: $gl-text-color;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
.dropdown-menu-inner-content {
@@ -724,30 +734,69 @@
overflow: hidden;
}
+@mixin dropdown-item-hover {
+ background-color: $dropdown-item-hover-bg;
+ color: $gl-text-color;
+}
+
// TODO: change global style and remove mixin
-@mixin new-style-dropdown {
- .dropdown-menu,
- .dropdown-menu-nav {
- .divider {
- margin: 6px 0;
- }
+@mixin new-style-dropdown($selector: '') {
+ #{$selector}.dropdown-menu,
+ #{$selector}.dropdown-menu-nav {
+ margin-bottom: 24px;
li {
+ display: block;
padding: 0 1px;
+ &:hover {
+ background-color: transparent;
+ }
+
+ &.divider {
+ margin: 6px 0;
+
+ &:hover {
+ background-color: $dropdown-divider-color;
+ }
+ }
+
&.dropdown-header {
padding: 8px 16px;
}
- a {
+ &.droplab-item-active button {
+ @include dropdown-item-hover;
+ }
+
+ a,
+ button,
+ .menu-item {
border-radius: 0;
+ box-shadow: none;
padding: 8px 16px;
+ text-align: left;
+ white-space: normal;
+ width: 100%;
+
+ // make sure the text color is not overriden
+ &.text-danger {
+ color: $brand-danger;
+ }
&.is-focused,
&:hover,
&:active,
&:focus {
- background-color: $gray-darker;
+ @include dropdown-item-hover;
+
+ background-color: $dropdown-item-hover-bg;
+ color: $gl-text-color;
+
+ // make sure the text color is not overriden
+ &.text-danger {
+ color: $brand-danger;
+ }
}
&.is-active {
@@ -756,6 +805,18 @@
&::before {
top: 16px;
}
+
+ &.dropdown-menu-user-link::before {
+ top: 50%;
+ transform: translateY(-50%);
+ }
+ }
+ }
+
+ &.dropdown-menu-empty-item a {
+ &:hover,
+ &:focus {
+ background-color: transparent;
}
}
}
@@ -773,7 +834,181 @@
}
}
- .dropdown-menu-align-right {
+ #{$selector}.dropdown-menu-align-right {
margin-top: 2px;
}
+
+ .open {
+ #{$selector}.dropdown-menu,
+ #{$selector}.dropdown-menu-nav {
+ @media (max-width: $screen-xs-max) {
+ max-width: 100%;
+ }
+ }
+ }
+}
+
+@media (max-width: $screen-xs-max) {
+ .navbar-gitlab {
+ li.header-projects,
+ li.header-more,
+ li.header-new,
+ li.header-user {
+ position: static;
+ }
+ }
+
+ header.navbar-gitlab .dropdown {
+ .dropdown-menu,
+ .dropdown-menu-nav {
+ width: 100%;
+ 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 {
+ padding: 0;
+}
+
+.projects-dropdown-container {
+ display: flex;
+ flex-direction: row;
+ width: 500px;
+ height: 334px;
+
+ .project-dropdown-sidebar,
+ .project-dropdown-content {
+ padding: 8px 0;
+ }
+
+ .loading-animation {
+ color: $almost-black;
+ }
+
+ .project-dropdown-sidebar {
+ width: 30%;
+ border-right: 1px solid $border-color;
+ }
+
+ .project-dropdown-content {
+ position: relative;
+ width: 70%;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ flex-direction: column;
+ width: 100%;
+ height: auto;
+ flex: 1;
+
+ .project-dropdown-sidebar,
+ .project-dropdown-content {
+ width: 100%;
+ }
+
+ .project-dropdown-sidebar {
+ border-bottom: 1px solid $border-color;
+ border-right: 0;
+ }
+ }
+}
+
+.projects-dropdown-container {
+ .projects-list-frequent-container,
+ .projects-list-search-container, {
+ padding: 8px 0;
+ overflow-y: auto;
+ }
+
+ .section-header,
+ .projects-list-frequent-container li.section-empty,
+ .projects-list-search-container li.section-empty {
+ padding: 0 15px;
+ }
+
+ .section-header,
+ .projects-list-frequent-container li.section-empty,
+ .projects-list-search-container li.section-empty {
+ color: $gl-text-color-secondary;
+ font-size: $gl-font-size;
+ }
+
+ .projects-list-frequent-container,
+ .projects-list-search-container {
+ li.section-empty.section-failure {
+ color: $callout-danger-color;
+ }
+ }
+
+ .search-input-container {
+ position: relative;
+ padding: 4px $gl-padding;
+
+ .search-icon {
+ position: absolute;
+ top: 13px;
+ right: 25px;
+ color: $md-area-border;
+ }
+ }
+
+ .section-header {
+ font-weight: 700;
+ margin-top: 8px;
+ }
+
+ .projects-list-search-container {
+ height: 284px;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ .projects-list-frequent-container {
+ width: auto;
+ height: auto;
+ padding-bottom: 0;
+ }
+ }
+}
+
+.projects-list-item-container {
+ .project-item-avatar-container
+ .project-item-metadata-container {
+ float: left;
+ }
+
+ .project-title,
+ .project-namespace {
+ max-width: 250px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &:hover {
+ .project-item-avatar-container .avatar {
+ border-color: $md-area-border;
+ }
+ }
+
+ .project-title {
+ font-size: $gl-font-size;
+ font-weight: 400;
+ line-height: 16px;
+ }
+
+ .project-namespace {
+ margin-top: 4px;
+ font-size: 12px;
+ line-height: 12px;
+ color: $gl-text-color-secondary;
+ }
+
+ @media (max-width: $screen-xs-max) {
+ .project-item-metadata-container {
+ float: none;
+ }
+ }
}
diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss
new file mode 100644
index 00000000000..ebae473df50
--- /dev/null
+++ b/app/assets/stylesheets/framework/feature_highlight.scss
@@ -0,0 +1,94 @@
+.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/filters.scss b/app/assets/stylesheets/framework/filters.scss
index ec13a86ccf7..b2847c348eb 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -50,6 +50,8 @@
}
.filtered-search-wrapper {
+ @include new-style-dropdown;
+
display: -webkit-flex;
display: flex;
@@ -223,6 +225,18 @@
color: $common-gray-dark;
}
+ gl-emoji {
+ display: inline-block;
+ font-family: inherit;
+ font-size: inherit;
+ vertical-align: inherit;
+
+ img {
+ height: 18px;
+ width: 18px;
+ }
+ }
+
.form-control {
position: relative;
min-width: 200px;
@@ -275,7 +289,7 @@
}
.filtered-search-input-dropdown-menu {
- max-height: 225px;
+ max-height: 260px;
max-width: 280px;
overflow: auto;
@@ -369,7 +383,7 @@
}
> .value {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
}
@@ -411,8 +425,6 @@
}
%filter-dropdown-item-btn-hover {
- background-color: $dropdown-hover-color;
- color: $white-light;
text-decoration: none;
outline: 0;
@@ -422,8 +434,6 @@
}
.droplab-dropdown .dropdown-menu .filter-dropdown-item {
- padding: 0;
-
.btn {
border: none;
width: 100%;
@@ -454,7 +464,7 @@
.dropdown-light-content {
font-size: 14px;
- font-weight: 400;
+ font-weight: $gl-font-weight-normal;
}
.dropdown-user {
@@ -480,3 +490,7 @@
padding: 8px 16px;
text-align: center;
}
+
+.issues-details-filters {
+ @include new-style-dropdown;
+}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 38d884bc7eb..e1b086ebb2b 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -25,7 +25,7 @@
a.flash-action {
margin-left: 5px;
text-decoration: none;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
border-bottom: 1px solid;
&:hover {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 61e3897f369..be96c8ee964 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -32,7 +32,7 @@ label {
}
&.label-light {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
}
@@ -73,7 +73,7 @@ label {
margin-right: 0;
.control-label {
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
padding-top: 4px;
}
@@ -157,7 +157,7 @@ label {
.form-group .control-label,
.form-group .control-label-full-width {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
.form-control::-webkit-input-placeholder {
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
new file mode 100644
index 00000000000..71f764923ff
--- /dev/null
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -0,0 +1,265 @@
+/**
+ * Styles the GitLab application with a specific color theme
+ */
+
+@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-collapse {
+ color: $color-200;
+ }
+
+ .container-fluid {
+ .navbar-toggle {
+ border-left: 1px solid lighten($color-700, 10%);
+ }
+ }
+
+ .navbar-sub-nav,
+ .navbar-nav {
+ > li {
+ > a:hover,
+ > a:focus {
+ background-color: rgba($color-200, .2);
+ }
+
+ &.active > a,
+ &.dropdown.open > a {
+ color: $color-900;
+ background-color: $color-alternate;
+
+ svg {
+ fill: currentColor;
+ }
+ }
+
+ &.line-separator {
+ border-left: 1px solid rgba($color-200, .2);
+ }
+ }
+ }
+
+ .navbar-sub-nav {
+ color: $color-200;
+ }
+
+ .nav {
+ > li {
+ color: $color-200;
+
+ > a {
+ svg {
+ fill: $color-200;
+ }
+
+ &.header-user-dropdown-toggle {
+ .header-user-avatar {
+ border-color: $color-200;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ @media (min-width: $screen-sm-min) {
+ background-color: rgba($color-200, .2);
+ }
+
+ svg {
+ fill: currentColor;
+ }
+ }
+ }
+
+ &.active > a,
+ &.dropdown.open > a {
+ color: $color-900;
+ background-color: $color-alternate;
+
+ &:hover {
+ svg {
+ fill: $color-900;
+ }
+ }
+ }
+
+ .impersonated-user,
+ .impersonated-user:hover {
+ svg {
+ fill: $color-900;
+ }
+ }
+ }
+ }
+ }
+
+ .title {
+ > a {
+ &:hover,
+ &:focus {
+ background-color: rgba($color-200, .2);
+ }
+ }
+ }
+
+ .search {
+ form {
+ background-color: rgba($color-200, .2);
+
+ &:hover {
+ background-color: rgba($color-200, .3);
+ }
+ }
+
+ .location-badge {
+ color: $color-100;
+ background-color: rgba($color-200, .1);
+ border-right: 1px solid $color-800;
+ }
+
+ .search-input::placeholder {
+ color: rgba($color-200, .8);
+ }
+
+ .search-input-wrap {
+ .search-icon,
+ .clear-icon {
+ color: rgba($color-200, .8);
+ }
+ }
+
+ &.search-active {
+ form {
+ background-color: $white-light;
+ }
+
+ .location-badge {
+ color: $gl-text-color;
+ }
+
+ .search-input-wrap {
+ .search-icon {
+ color: rgba($color-200, .8);
+ }
+ }
+ }
+ }
+
+ .btn-sign-in {
+ background-color: $color-100;
+ color: $color-900;
+ }
+
+
+ // Sidebar
+ .nav-sidebar li.active {
+ box-shadow: inset 4px 0 0 $color-700;
+
+ > a {
+ color: $color-900;
+ }
+
+ svg {
+ fill: $color-900;
+ }
+ }
+}
+
+
+body {
+ &.ui_indigo {
+ @include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light);
+ }
+
+ &.ui_dark {
+ @include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light);
+ }
+
+ &.ui_blue {
+ @include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light);
+ }
+
+ &.ui_green {
+ @include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light);
+ }
+
+ &.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;
+
+ .logo-text svg {
+ fill: $theme-gray-900;
+ }
+
+ .navbar-sub-nav,
+ .navbar-nav {
+ > li {
+ > a:hover,
+ > a:focus {
+ color: $theme-gray-900;
+ }
+
+ &.active > a {
+ color: $white-light;
+
+ &:hover {
+ color: $white-light;
+ }
+ }
+ }
+ }
+
+ .container-fluid {
+ .navbar-toggle,
+ .navbar-toggle:hover {
+ color: $theme-gray-700;
+ border-left: 1px solid $theme-gray-200;
+ }
+ }
+ }
+
+ .search {
+ form {
+ background-color: $white-light;
+ box-shadow: inset 0 0 0 1px $border-color;
+
+ &:hover {
+ background-color: $white-light;
+ box-shadow: inset 0 0 0 1px $blue-100;
+
+ .location-badge {
+ box-shadow: inset 0 0 0 1px $blue-100;
+ }
+ }
+ }
+
+ .search-input-wrap {
+ .search-icon {
+ color: $theme-gray-200;
+ }
+ }
+
+ .location-badge {
+ color: $theme-gray-700;
+ box-shadow: inset 0 0 0 1px $border-color;
+ background-color: $nav-badge-bg;
+ border-right: 0;
+ }
+ }
+
+ .nav-sidebar li.active {
+ > a {
+ color: $theme-gray-900;
+ }
+
+ svg {
+ fill: $theme-gray-900;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index d9f92e93160..ab3c34df1fb 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -26,7 +26,7 @@ header {
&.navbar-gitlab {
padding: 0 16px;
- z-index: 2000;
+ z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
background-color: $gray-light;
@@ -105,14 +105,12 @@ header {
top: -3px;
font-size: 10px;
}
+ }
+ .user-counter {
svg {
- position: relative;
- top: 2px;
- height: 17px;
- // hack to get SVG to line up with FA icons
+ height: 16px;
width: 23px;
- fill: currentColor;
}
}
@@ -160,7 +158,7 @@ header {
li {
&.active a {
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
}
}
@@ -250,7 +248,7 @@ header {
font-size: 18px;
line-height: 22px;
display: inline-block;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
color: $gl-text-color;
vertical-align: top;
white-space: nowrap;
@@ -325,12 +323,12 @@ header {
li {
.badge {
position: inherit;
- top: -8px;
- font-weight: normal;
- margin-left: -11px;
+ font-weight: $gl-font-weight-normal;
+ margin-left: -6px;
font-size: 11px;
color: $white-light;
- padding: 1px 5px 2px;
+ padding: 0 5px;
+ line-height: 12px;
border-radius: 7px;
box-shadow: 0 1px 0 rgba($gl-header-color, .2);
diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss
index 71d5949b023..c63114f85b4 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -47,7 +47,7 @@
font-family: $monospace_font;
display: block;
font-size: $code_font_size !important;
- line-height: 19px;
+ min-height: 19px;
white-space: nowrap;
i {
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index bd0367f86dd..bd521028c44 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -117,10 +117,6 @@ body {
margin-top: $header-height + $performance-bar-height;
}
-[v-cloak] {
- display: none;
-}
-
.vertical-center {
min-height: 100vh;
display: flex;
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index ab754f4a492..0fb19344510 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -113,7 +113,7 @@ ul.content-list {
}
.title {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
a {
@@ -132,6 +132,8 @@ ul.content-list {
}
.controls {
+ @include new-style-dropdown;
+
float: right;
> .control-text {
@@ -210,7 +212,7 @@ ul.content-list {
}
.row-title {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
.row-second-line {
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index fcd4c72b430..e3920b5d3d9 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -204,6 +204,16 @@
}
}
+ div.avatar {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+
+ .center {
+ line-height: 14px;
+ }
+ }
+
strong {
color: $gl-text-color;
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 6f91d11b369..d40b65bb2cc 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -43,7 +43,7 @@
background: $gray-light;
a {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index a28f54936be..5b581780447 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -8,7 +8,7 @@
}
.text-danger {
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
}
@@ -16,6 +16,14 @@ body.modal-open {
overflow: hidden;
}
+.modal-no-backdrop {
+ @extend .modal-dialog;
+
+ .modal-content {
+ box-shadow: none;
+ }
+}
+
@media (min-width: $screen-md-min) {
.modal-dialog {
width: 860px;
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index d386ac5ba9c..5ffa67a1220 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -70,7 +70,7 @@
&.active a {
border-bottom: 2px solid $link-underline-blue;
color: $black;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
.badge {
color: $black;
@@ -161,6 +161,8 @@
}
.nav-controls {
+ @include new-style-dropdown;
+
display: inline-block;
float: right;
text-align: right;
@@ -286,11 +288,7 @@
display: flex;
max-width: 350px;
overflow: hidden;
-
- @media(max-width: $screen-xs-max) {
- width: 100%;
- max-width: none;
- }
+ float: right;
.new-project-item-link {
white-space: nowrap;
@@ -303,6 +301,23 @@
}
}
+.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;
}
@@ -350,7 +365,7 @@
z-index: 300;
li.active {
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
}
}
diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss
index f1ecd050a0a..0c879f40930 100644
--- a/app/assets/stylesheets/framework/page-header.scss
+++ b/app/assets/stylesheets/framework/page-header.scss
@@ -43,7 +43,7 @@
.commit-committer-link,
.commit-author-link {
color: $gl-text-color;
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
.commit-info {
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 40e654f4838..6c14e8b97e0 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -76,7 +76,7 @@
}
.select2-results li.select2-result-with-children > .select2-result-label {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
color: $gl-text-color;
}
@@ -227,7 +227,7 @@
}
.group-name {
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
.group-path {
@@ -252,15 +252,69 @@
.namespace-result {
.namespace-kind {
color: $namespace-kind-color;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
.namespace-path {
margin-left: 10px;
- font-weight: bolder;
+ font-weight: $gl-font-weight-bold;
}
}
.ajax-users-dropdown {
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;
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 40e8a928e6e..ef58382ba41 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -132,3 +132,7 @@
width: calc(100% + 35px);
}
}
+
+.issuable-sidebar {
+ @include new-style-dropdown;
+}
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 5f7e1b17cc7..30c15c231d5 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -30,7 +30,7 @@
.snippet-title {
font-size: 24px;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
.snippet-edited-ago {
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 6d9fa74a030..4dd31bf28cd 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -32,7 +32,7 @@ table {
th {
background-color: $gray-light;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
border-bottom: none;
&.wide {
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index e54cc2866a7..d5c6ddbb4a5 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -103,7 +103,7 @@ summary {
padding: 4px 5px;
font-size: 12px;
font-style: normal;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
display: inline-block;
&.label-gray {
@@ -165,7 +165,7 @@ summary {
.panel-heading {
padding: 6px 15px;
font-size: 13px;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
a {
color: $panel-heading-link-color;
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index b666223b120..4c35e3a9c3c 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -162,3 +162,5 @@ $pre-color: $gl-text-color !default;
$pre-border-color: $border-color;
$table-bg-accent: $gray-light;
+
+$zindex-popover: 900;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index bf5f124d142..3c0b4c82d19 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -10,18 +10,22 @@
color: $md-link-color;
}
- img {
- /*max-width: 100%;*/
+ img:not(.emoji) {
margin: 0 0 8px;
+ }
+
+ img.lazy {
min-width: 200px;
min-height: 100px;
background-color: $gray-lightest;
}
- img.js-lazy-loaded {
+ img.js-lazy-loaded,
+ img.emoji {
min-width: inherit;
min-height: inherit;
background-color: inherit;
+ max-width: 100%;
}
p a:not(.no-attachment-icon) img {
@@ -70,7 +74,7 @@
h1 {
font-size: 1.75em;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
margin: 24px 0 16px;
padding-bottom: 0.3em;
border-bottom: 1px solid $white-dark;
@@ -83,7 +87,7 @@
h2 {
font-size: 1.5em;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
margin: 24px 0 16px;
padding-bottom: 0.3em;
border-bottom: 1px solid $white-dark;
@@ -276,7 +280,7 @@ body {
margin-top: $gl-padding;
line-height: 1.3;
font-size: 1.25em;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
&:last-child {
margin-bottom: 0;
@@ -287,7 +291,7 @@ body {
margin-top: 0;
line-height: 1.3;
font-size: 1.25em;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
margin: 12px 7px;
}
@@ -298,11 +302,11 @@ h4,
h5,
h6 {
color: $gl-text-color;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
.light-header {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
/** CODE **/
@@ -339,6 +343,8 @@ a > code {
@extend .ref-name;
}
+@include new-style-dropdown('.git-revision-dropdown');
+
/**
* Apply Markdown typography
*
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 3c109a5a929..3857226cddb 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -74,6 +74,8 @@ $red-700: #a62d19;
$red-800: #8b2615;
$red-900: #711e11;
+// GitLab themes
+
$indigo-50: #f7f7ff;
$indigo-100: #ebebfa;
$indigo-200: #d1d1f0;
@@ -86,6 +88,43 @@ $indigo-800: #393982;
$indigo-900: #292961;
$indigo-950: #1a1a40;
+$theme-gray-50: #fafafa;
+$theme-gray-100: #f2f2f2;
+$theme-gray-200: #dfdfdf;
+$theme-gray-300: #cccccc;
+$theme-gray-400: #bababa;
+$theme-gray-500: #a7a7a7;
+$theme-gray-600: #949494;
+$theme-gray-700: #707070;
+$theme-gray-800: #4f4f4f;
+$theme-gray-900: #2e2e2e;
+$theme-gray-950: #1f1f1f;
+
+$theme-blue-50: #f4f8fc;
+$theme-blue-100: #e6edf5;
+$theme-blue-200: #c8d7e6;
+$theme-blue-300: #97b3cf;
+$theme-blue-400: #648cb4;
+$theme-blue-500: #4a79a8;
+$theme-blue-600: #3e6fa0;
+$theme-blue-700: #305c88;
+$theme-blue-800: #25496e;
+$theme-blue-900: #1a3652;
+$theme-blue-950: #0f2235;
+
+$theme-green-50: #f2faf6;
+$theme-green-100: #e4f3ea;
+$theme-green-200: #c0dfcd;
+$theme-green-300: #8ac2a1;
+$theme-green-400: #52a274;
+$theme-green-500: #35935c;
+$theme-green-600: #288a50;
+$theme-green-700: #1c7441;
+$theme-green-800: #145d33;
+$theme-green-900: #0d4524;
+$theme-green-950: #072d16;
+
+
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424;
@@ -111,6 +150,8 @@ $well-light-text-color: #5b6169;
* Text
*/
$gl-font-size: 14px;
+$gl-font-weight-normal: 400;
+$gl-font-weight-bold: 600;
$gl-text-color: #2e2e2e;
$gl-text-color-secondary: #707070;
$gl-text-color-tertiary: #949494;
@@ -118,6 +159,7 @@ $gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1.0);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-green: $green-600;
+$gl-text-green-hover: $green-700;
$gl-text-red: $red-500;
$gl-text-orange: $orange-600;
$gl-link-color: $blue-600;
@@ -174,13 +216,14 @@ $row-hover: $blue-25;
$row-hover-border: $blue-100;
$progress-color: #c0392b;
$header-height: 50px;
+$new-navbar-height: 40px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$limited-layout-width-sm: 790px;
$container-text-max-width: 540px;
$gl-avatar-size: 40px;
$error-exclamation-point: $red-500;
-$border-radius-default: 3px;
+$border-radius-default: 4px;
$settings-icon-size: 18px;
$provider-btn-not-active-color: $blue-500;
$link-underline-blue: $blue-500;
@@ -294,7 +337,7 @@ $dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4);
$dropdown-loading-bg: rgba(#fff, .6);
$dropdown-chevron-size: 10px;
$dropdown-toggle-active-border-color: darken($border-color, 14%);
-
+$dropdown-item-hover-bg: $gray-darker;
/*
* Filtered Search
@@ -536,6 +579,11 @@ $project-breadcrumb-color: #999;
$project-private-forks-notice-odd: $green-600;
$project-network-controls-color: #888;
+$feature-toggle-color: #fff;
+$feature-toggle-text-color: #fff;
+$feature-toggle-color-disabled: #999;
+$feature-toggle-color-enabled: #4a8bee;
+
/*
* Runners
*/
@@ -588,9 +636,10 @@ $ui-dev-kit-example-border: #ddd;
/*
Pipeline Graph
*/
-$stage-hover-bg: #eaf3fc;
-$stage-hover-border: #d1e7fc;
-$action-icon-color: #d6d6d6;
+$stage-hover-bg: $gray-darker;
+$ci-action-icon-size: 22px;
+$pipeline-dropdown-line-height: 20px;
+$pipeline-dropdown-status-icon-size: 18px;
/*
Pipeline Schedules
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index b1ff2659131..5f9756bf58a 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -69,7 +69,7 @@
.well-centered {
h1 {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
text-align: center;
font-size: 48px;
}
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 6e3829d994f..f0ac9b46f91 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -204,11 +204,11 @@ $dark-il: #de935f;
.cs { color: $dark-cs; } /* Comment.Special */
.gd { color: $dark-gd; } /* Generic.Deleted */
.ge { font-style: italic; } /* Generic.Emph */
- .gh { color: $dark-gh; font-weight: bold; } /* Generic.Heading */
+ .gh { color: $dark-gh; font-weight: $gl-font-weight-bold; } /* Generic.Heading */
.gi { color: $dark-gi; } /* Generic.Inserted */
- .gp { color: $dark-gp; font-weight: bold; } /* Generic.Prompt */
- .gs { font-weight: bold; } /* Generic.Strong */
- .gu { color: $dark-gu; font-weight: bold; } /* Generic.Subheading */
+ .gp { color: $dark-gp; font-weight: $gl-font-weight-bold; } /* Generic.Prompt */
+ .gs { font-weight: $gl-font-weight-bold; } /* Generic.Strong */
+ .gu { color: $dark-gu; font-weight: $gl-font-weight-bold; } /* Generic.Subheading */
.kc { color: $dark-kc; } /* Keyword.Constant */
.kd { color: $dark-kd; } /* Keyword.Declaration */
.kn { color: $dark-kn; } /* Keyword.Namespace */
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 68eb0c7720f..eba7919ada9 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -203,7 +203,7 @@ $monokai-gi: #a6e22e;
.c1 { color: $monokai-c1; } /* Comment.Single */
.cs { color: $monokai-cs; } /* Comment.Special */
.ge { font-style: italic; } /* Generic.Emph */
- .gs { font-weight: bold; } /* Generic.Strong */
+ .gs { font-weight: $gl-font-weight-bold; } /* Generic.Strong */
.kc { color: $monokai-kc; } /* Keyword.Constant */
.kd { color: $monokai-kd; } /* Keyword.Declaration */
.kn { color: $monokai-kn; } /* Keyword.Namespace */
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 2cc968c32f2..ba53ef0352b 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -231,7 +231,7 @@ $solarized-dark-il: #2aa198;
.gi { color: $solarized-dark-gi; } /* Generic.Inserted */
.go { color: $solarized-dark-go; } /* Generic.Output */
.gp { color: $solarized-dark-gp; } /* Generic.Prompt */
- .gs { color: $solarized-dark-gs; font-weight: bold; } /* Generic.Strong */
+ .gs { color: $solarized-dark-gs; font-weight: $gl-font-weight-bold; } /* Generic.Strong */
.gu { color: $solarized-dark-gu; } /* Generic.Subheading */
.gt { color: $solarized-dark-gt; } /* Generic.Traceback */
.kc { color: $solarized-dark-kc; } /* Keyword.Constant */
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index b61b85a2cd1..e9fccf1b58a 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -239,7 +239,7 @@ $solarized-light-il: #2aa198;
.gi { color: $solarized-light-gi; } /* Generic.Inserted */
.go { color: $solarized-light-go; } /* Generic.Output */
.gp { color: $solarized-light-gp; } /* Generic.Prompt */
- .gs { color: $solarized-light-gs; font-weight: bold; } /* Generic.Strong */
+ .gs { color: $solarized-light-gs; font-weight: $gl-font-weight-bold; } /* Generic.Strong */
.gu { color: $solarized-light-gu; } /* Generic.Subheading */
.gt { color: $solarized-light-gt; } /* Generic.Traceback */
.kc { color: $solarized-light-kc; } /* Keyword.Constant */
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 578f1902cce..65b140cd7f8 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -211,12 +211,12 @@ $white-gc-bg: #eaf2f5;
.hll { background-color: $white-hll-bg; }
.c { color: $white-c; font-style: italic; }
.err { color: $white-err; background-color: $white-err-bg; }
- .k { font-weight: bold; }
- .o { font-weight: bold; }
+ .k { font-weight: $gl-font-weight-bold; }
+ .o { font-weight: $gl-font-weight-bold; }
.cm { color: $white-cm; font-style: italic; }
- .cp { color: $white-cp; font-weight: bold; }
+ .cp { color: $white-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $white-c1; font-style: italic; }
- .cs { color: $white-cs; font-weight: bold; font-style: italic; }
+ .cs { color: $white-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
.gd { color: $white-gd; background-color: $white-gd-bg; }
.gd .x { color: $white-gd-x; background-color: $white-gd-x-bg; }
.ge { font-style: italic; }
@@ -226,29 +226,29 @@ $white-gc-bg: #eaf2f5;
.gi .x { color: $white-gi-x; background-color: $white-gi-x-bg; }
.go { color: $white-go; }
.gp { color: $white-gp; }
- .gs { font-weight: bold; }
- .gu { color: $white-gu; font-weight: bold; }
+ .gs { font-weight: $gl-font-weight-bold; }
+ .gu { color: $white-gu; font-weight: $gl-font-weight-bold; }
.gt { color: $white-gt; }
- .kc { font-weight: bold; }
- .kd { font-weight: bold; }
- .kn { font-weight: bold; }
- .kp { font-weight: bold; }
- .kr { font-weight: bold; }
- .kt { color: $white-kt; font-weight: bold; }
+ .kc { font-weight: $gl-font-weight-bold; }
+ .kd { font-weight: $gl-font-weight-bold; }
+ .kn { font-weight: $gl-font-weight-bold; }
+ .kp { font-weight: $gl-font-weight-bold; }
+ .kr { font-weight: $gl-font-weight-bold; }
+ .kt { color: $white-kt; font-weight: $gl-font-weight-bold; }
.m { color: $white-m; }
.s { color: $white-s; }
.n { color: $white-n; }
.na { color: $white-na; }
.nb { color: $white-nb; }
- .nc { color: $white-nc; font-weight: bold; }
+ .nc { color: $white-nc; font-weight: $gl-font-weight-bold; }
.no { color: $white-no; }
.ni { color: $white-ni; }
- .ne { color: $white-ne; font-weight: bold; }
- .nf { color: $white-nf; font-weight: bold; }
+ .ne { color: $white-ne; font-weight: $gl-font-weight-bold; }
+ .nf { color: $white-nf; font-weight: $gl-font-weight-bold; }
.nn { color: $white-nn; }
.nt { color: $white-nt; }
.nv { color: $white-nv; }
- .ow { font-weight: bold; }
+ .ow { font-weight: $gl-font-weight-bold; }
.w { color: $white-w; }
.mf { color: $white-mf; }
.mh { color: $white-mh; }
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index ea40f449134..fbe538ad1d7 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -152,12 +152,12 @@ span.highlight_word {
.hll { background-color: $highlighted-hll-bg; }
.c { color: $highlighted-c; font-style: italic; }
.err { color: $highlighted-err; background-color: $highlighted-err-bg; }
-.k { font-weight: bold; }
-.o { font-weight: bold; }
+.k { font-weight: $gl-font-weight-bold; }
+.o { font-weight: $gl-font-weight-bold; }
.cm { color: $highlighted-cm; font-style: italic; }
-.cp { color: $highlighted-cp; font-weight: bold; }
+.cp { color: $highlighted-cp; font-weight: $gl-font-weight-bold; }
.c1 { color: $highlighted-c1; font-style: italic; }
-.cs { color: $highlighted-cs; font-weight: bold; font-style: italic; }
+.cs { color: $highlighted-cs; font-weight: $gl-font-weight-bold; font-style: italic; }
.gd { color: $highlighted-gd; background-color: $highlighted-gd-bg; }
.gd .x { color: $highlighted-gd; background-color: $highlighted-gd-x-bg; }
.ge { font-style: italic; }
@@ -167,29 +167,29 @@ span.highlight_word {
.gi .x { color: $highlighted-gi; background-color: $highlighted-gi-x-bg; }
.go { color: $highlighted-go; }
.gp { color: $highlighted-gp; }
-.gs { font-weight: bold; }
-.gu { color: $highlighted-gu; font-weight: bold; }
+.gs { font-weight: $gl-font-weight-bold; }
+.gu { color: $highlighted-gu; font-weight: $gl-font-weight-bold; }
.gt { color: $highlighted-gt; }
-.kc { font-weight: bold; }
-.kd { font-weight: bold; }
-.kn { font-weight: bold; }
-.kp { font-weight: bold; }
-.kr { font-weight: bold; }
-.kt { color: $highlighted-kt; font-weight: bold; }
+.kc { font-weight: $gl-font-weight-bold; }
+.kd { font-weight: $gl-font-weight-bold; }
+.kn { font-weight: $gl-font-weight-bold; }
+.kp { font-weight: $gl-font-weight-bold; }
+.kr { font-weight: $gl-font-weight-bold; }
+.kt { color: $highlighted-kt; font-weight: $gl-font-weight-bold; }
.m { color: $highlighted-m; }
.s { color: $highlighted-s; }
.n { color: $highlighted-n; }
.na { color: $highlighted-na; }
.nb { color: $highlighted-nb; }
-.nc { color: $highlighted-nc; font-weight: bold; }
+.nc { color: $highlighted-nc; font-weight: $gl-font-weight-bold; }
.no { color: $highlighted-no; }
.ni { color: $highlighted-ni; }
-.ne { color: $highlighted-ne; font-weight: bold; }
-.nf { color: $highlighted-nf; font-weight: bold; }
+.ne { color: $highlighted-ne; font-weight: $gl-font-weight-bold; }
+.nf { color: $highlighted-nf; font-weight: $gl-font-weight-bold; }
.nn { color: $highlighted-nn; }
.nt { color: $highlighted-nt; }
.nv { color: $highlighted-nv; }
-.ow { font-weight: bold; }
+.ow { font-weight: $gl-font-weight-bold; }
.w { color: $highlighted-w; }
.mf { color: $highlighted-mf; }
.mh { color: $highlighted-mh; }
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index 795ee91af8b..8e095cbdd7e 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -1,66 +1,67 @@
@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;
- background: linear-gradient(to right, $indigo-900, $indigo-800);
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: 10px;
+ margin-right: 8px;
}
- > a {
+ a {
+ display: -webkit-flex;
display: flex;
align-items: center;
- padding-right: $gl-padding;
- padding-left: $gl-padding;
- margin-left: -$gl-padding;
-
- @media (min-width: $screen-sm-min) {
- padding-right: $gl-padding;
- padding-left: $gl-padding;
- }
+ padding: 2px 8px;
+ margin: 5px 2px 5px -8px;
+ border-radius: $border-radius-default;
svg {
- margin-top: -3px;
-
@media (min-width: $screen-sm-min) {
- margin-right: 10px;
- }
- }
-
- .logo-text {
- line-height: initial;
-
- svg {
- width: 55px;
- height: 15px;
- margin: 0;
- fill: $white-light;
- }
- }
-
- &:hover,
- &:focus {
- .logo-text svg {
- fill: $tanuki-yellow;
+ margin-right: 8px;
}
}
}
@@ -81,22 +82,31 @@ header.navbar-gitlab-new {
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;
- color: $indigo-200;
box-shadow: 0;
@media (max-width: $screen-xs-max) {
- margin-left: -$gl-padding;
+ margin-left: -8px;
margin-right: -10px;
}
- .dropdown-bold-header {
- color: initial;
- }
-
.nav {
> li:not(.hidden-xs) a {
@media (max-width: $screen-xs-max) {
@@ -110,12 +120,11 @@ header.navbar-gitlab-new {
.container-fluid {
.navbar-toggle {
min-width: 45px;
- padding: 6px $gl-padding;
+ padding: 4px $gl-padding;
margin-right: -7px;
font-size: 14px;
text-align: center;
color: currentColor;
- border-left: 1px solid lighten($indigo-700, 10%);
&:hover,
&:focus,
@@ -134,7 +143,7 @@ header.navbar-gitlab-new {
li {
.badge {
box-shadow: none;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
}
}
@@ -147,22 +156,32 @@ header.navbar-gitlab-new {
}
> a {
- background: none;
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 {
- border-color: $indigo-200;
+ margin-right: 0;
}
}
&:hover,
&:focus {
- color: $white-light;
+ text-decoration: none;
+ outline: 0;
opacity: 1;
+ color: $white-light;
- > svg {
- fill: $white-light;
+ svg {
+ fill: currentColor;
}
&.header-user-dropdown-toggle {
@@ -172,45 +191,47 @@ header.navbar-gitlab-new {
}
}
}
- }
- }
-}
-.navbar-sub-nav {
- display: flex;
- margin-bottom: 0;
- color: $indigo-200;
+ .header-new-dropdown-toggle {
+ margin-right: 0;
+ }
- > li {
- > a:hover,
- > a:focus {
- box-shadow: inset 0 -3px 0 rgba($indigo-200, .4);
- text-decoration: none;
- outline: 0;
- color: $white-light;
- }
+ .impersonated-user,
+ .impersonated-user:hover {
+ margin-right: 1px;
+ background-color: $white-light;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
- &.active > a {
- box-shadow: inset 0 -3px 0 $indigo-500;
- color: $white-light;
- font-weight: 700;
- }
+ .impersonation-btn,
+ .impersonation-btn:hover {
+ background-color: $white-light;
+ margin-left: 0;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
- > a {
- display: block;
- padding: 16px 10px;
- font-size: 13px;
- color: currentColor;
- box-shadow: inset 0 0 0 transparent;
- will-change: box-shadow;
- transition: box-shadow 0.15s;
+ i {
+ color: $orange-500;
+ font-size: 20px;
+ }
+ }
+
+ &.active > a,
+ &.dropdown.open > a {
- @media (min-width: $screen-sm-min) {
- padding: 15px $gl-padding;
- font-size: 14px;
+ svg {
+ fill: currentColor;
+ }
}
}
}
+}
+
+.navbar-sub-nav {
+ display: -webkit-flex;
+ display: flex;
+ margin: 0 0 0 6px;
.dropdown-chevron {
position: relative;
@@ -219,69 +240,123 @@ header.navbar-gitlab-new {
}
}
+.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;
- background-color: rgba($indigo-200, .2);
- transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, background-color ease-in-out 0.15s;
+ border-radius: $border-radius-default;
+ transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
- background-color: rgba($indigo-200, .3);
box-shadow: none;
}
}
&.search-active form {
- background-color: rgba($indigo-200, .3);
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 {
- color: rgba($indigo-200, .8);
+ transition: color ease-in-out 0.15s;
}
.location-badge {
font-size: 12px;
- color: $indigo-100;
- background-color: rgba($indigo-200, .1);
- transition: color 0.15s;
- will-change: color;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: 2px 0 0 2px;
- border-right: 1px solid $indigo-800;
- height: 34px;
- }
-
- .search-input-wrap {
- .search-icon,
- .clear-icon {
- color: rgba($indigo-200, .8);
- }
+ height: 32px;
+ transition: border-color ease-in-out 0.15s;
}
&.search-active {
.location-badge {
- color: $white-light;
- background-color: rgba($indigo-200, .2);
+ background-color: $nav-badge-bg;
+ border-color: $border-color;
}
.search-input-wrap {
- .search-icon {
- color: rgba($indigo-200, .8);
- }
-
.clear-icon {
color: $white-light;
}
@@ -291,109 +366,38 @@ header.navbar-gitlab-new {
.breadcrumbs {
display: flex;
- min-height: 61px;
+ min-height: 48px;
color: $gl-text-color;
- border-bottom: 1px solid $border-color;
-
- .dropdown-toggle-caret {
- position: relative;
- top: -1px;
- padding: 0 5px;
- color: $gl-text-color-secondary;
- font-size: 10px;
- line-height: 1;
- background: none;
- border: 0;
-
- &:focus {
- outline: 0;
- }
- }
-
- // TODO: fallback to global style
- .dropdown-menu {
- .divider {
- margin: 6px 0;
- }
-
- li {
- padding: 0 1px;
-
- a {
- border-radius: 0;
- padding: 8px 16px;
-
- &.is-focused,
- &:hover,
- &:active,
- &:focus {
- background-color: $gray-darker;
- }
- }
- }
- }
}
.breadcrumbs-container {
+ display: -webkit-flex;
display: flex;
width: 100%;
position: relative;
+ padding-top: $gl-padding;
+ padding-bottom: $gl-padding;
align-items: center;
-
- .dropdown-menu-projects {
- margin-top: -$gl-padding;
- margin-left: $gl-padding;
- }
+ border-bottom: 1px solid $border-color;
}
.breadcrumbs-links {
+ -webkit-flex: 1;
flex: 1;
min-width: 0;
align-self: center;
- color: $gl-text-color-quaternary;
-
- a {
- color: $gl-text-color-secondary;
-
- &:not(:first-child),
- &.group-path {
- margin-left: 4px;
- }
-
- &:not(:last-of-type),
- &.group-path {
- margin-right: 3px;
- }
- }
-
- .title {
- display: inline-block;
-
- > a {
- &:last-of-type:not(:first-child) {
- font-weight: 600;
- }
- }
- }
+ color: $gl-text-color-secondary;
.avatar-tile {
- margin-right: 5px;
+ margin-right: 4px;
border: 1px solid $border-color;
border-radius: 50%;
vertical-align: sub;
-
- &.identicon {
- float: left;
- width: 16px;
- height: 16px;
- margin-top: 2px;
- font-size: 10px;
- }
}
.text-expander {
- margin-left: 4px;
- margin-right: 4px;
+ margin-left: 0;
+ margin-right: 2px;
> i {
position: relative;
@@ -402,48 +406,63 @@ header.navbar-gitlab-new {
}
}
-.breadcrumbs-extra {
- flex: 0 0 auto;
- margin-left: auto;
-}
-
-.breadcrumbs-sub-title {
- margin: 2px 0;
- font-size: 16px;
- font-weight: normal;
- line-height: 1;
-
- ul {
- margin: 0;
- }
+.breadcrumbs-list {
+ display: -webkit-flex;
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 0;
+ line-height: 16px;
- li {
- display: inline-block;
+ > li {
+ display: flex;
+ align-items: center;
+ position: relative;
&:not(:last-child) {
- &::after {
- content: "/";
- margin: 0 2px 0 5px;
- color: rgba($black, .65);
- }
+ margin-right: 20px;
}
- &:last-child a {
- font-weight: 600;
+ > 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;
}
}
-.top-area {
- .nav-controls-new-nav {
- .dropdown {
- @media (min-width: $screen-sm-min) {
- margin-right: 0;
- }
- }
+.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
index 76dccd2df56..4bbd30056a9 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -26,7 +26,7 @@ $new-sidebar-collapsed-width: 50px;
// Override position: absolute
.right-sidebar {
position: fixed;
- height: calc(100% - #{$header-height});
+ height: calc(100% - #{$new-navbar-height});
}
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
@@ -45,8 +45,7 @@ $new-sidebar-collapsed-width: 50px;
margin-right: 2px;
a {
- border-bottom: 1px solid $border-color;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
display: flex;
align-items: center;
padding: 10px 16px 10px 10px;
@@ -70,8 +69,7 @@ $new-sidebar-collapsed-width: 50px;
background-color: $white-light;
}
- .project-title,
- .group-title {
+ .sidebar-context-title {
overflow: hidden;
text-overflow: ellipsis;
}
@@ -94,21 +92,33 @@ $new-sidebar-collapsed-width: 50px;
z-index: 400;
width: $new-sidebar-width;
transition: left $sidebar-transition-duration;
- top: $header-height;
+ top: $new-navbar-height;
bottom: 0;
left: 0;
- overflow: auto;
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-item-name,
- .badge,
- .project-title {
+ .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 {
@@ -145,15 +155,8 @@ $new-sidebar-collapsed-width: 50px;
}
li.active {
- box-shadow: inset 4px 0 0 $active-border;
-
> a {
- color: $active-color;
- font-weight: 700;
- }
-
- svg {
- fill: $active-color;
+ font-weight: $gl-font-weight-bold;
}
}
@@ -170,10 +173,20 @@ $new-sidebar-collapsed-width: 50px;
width: 16px;
}
}
+
+ .fly-out-top-item {
+ display: none;
+ }
+}
+
+.nav-sidebar-inner-scroll {
+ height: 100%;
+ width: 100%;
+ overflow: auto;
}
.with-performance-bar .nav-sidebar {
- top: $header-height + $performance-bar-height;
+ top: $new-navbar-height + $performance-bar-height;
}
.sidebar-sub-level-items {
@@ -182,7 +195,7 @@ $new-sidebar-collapsed-width: 50px;
> li {
a {
- padding: 8px 16px 8px 50px;
+ padding: 8px 16px 8px 40px;
&:hover,
&:focus {
@@ -215,12 +228,15 @@ $new-sidebar-collapsed-width: 50px;
&:hover {
color: $gl-text-color;
+
+ svg {
+ fill: $gl-text-color;
+ }
}
}
- &:not(.active) {
+ &.is-showing-fly-out {
> a {
- margin-left: 1px;
margin-right: 2px;
}
@@ -229,9 +245,9 @@ $new-sidebar-collapsed-width: 50px;
position: fixed;
top: 0;
left: $new-sidebar-width;
- width: 150px;
+ min-width: 150px;
margin-top: -1px;
- padding: 8px 1px;
+ padding: 4px 1px;
background-color: $white-light;
box-shadow: 2px 1px 3px $dropdown-shadow-color;
border: 1px solid $gray-darker;
@@ -243,31 +259,27 @@ $new-sidebar-collapsed-width: 50px;
position: absolute;
top: -30px;
bottom: -30px;
- left: 0;
+ left: -10px;
right: -30px;
z-index: -1;
}
- &::after {
- content: "";
- position: absolute;
- top: 44px;
- left: -30px;
- right: 35px;
- bottom: 0;
- height: 100%;
- max-height: 150px;
- z-index: -1;
- transform: skew(33deg);
- }
-
&.is-above {
margin-top: 1px;
+ }
+
+ .divider {
+ height: 1px;
+ margin: 4px -1px;
+ padding: 0;
+ background-color: $dropdown-divider-color;
+ }
- &::after {
- top: auto;
- bottom: 44px;
- transform: skew(-30deg);
+ > .active {
+ box-shadow: none;
+
+ > a {
+ background-color: transparent;
}
}
@@ -294,20 +306,20 @@ $new-sidebar-collapsed-width: 50px;
> a {
margin-left: 4px;
+ padding-left: 12px;
}
.badge {
color: $active-color;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
- .sidebar-sub-level-items {
+ .sidebar-sub-level-items:not(.is-fly-out-only) {
display: block;
}
}
- &:not(.active):hover > a,
- > a:hover,
+ &.active > a:hover,
&.is-over > a {
background-color: $white-light;
}
@@ -354,7 +366,7 @@ $new-sidebar-collapsed-width: 50px;
.sidebar-icons-only {
.context-header {
- height: 60px;
+ height: 61px;
a {
padding: 10px 4px;
@@ -381,6 +393,10 @@ $new-sidebar-collapsed-width: 50px;
}
}
+ .nav-icon-container {
+ margin-right: 0;
+ }
+
.toggle-sidebar-button {
width: $new-sidebar-collapsed-width - 2px;
padding: 16px 18px;
@@ -396,6 +412,19 @@ $new-sidebar-collapsed-width: 50px;
}
}
+.fly-out-top-item {
+ > a {
+ display: flex;
+ }
+
+ .fly-out-badge {
+ margin-left: 8px;
+ }
+}
+
+.fly-out-top-item-name {
+ flex: 1;
+}
// Mobile nav
@@ -445,7 +474,7 @@ $new-sidebar-collapsed-width: 50px;
// Make issue boards full-height now that sub-nav is gone
.boards-list {
- height: calc(100vh - #{$header-height});
+ height: calc(100vh - #{$new-navbar-height});
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
@@ -456,7 +485,7 @@ $new-sidebar-collapsed-width: 50px;
}
.with-performance-bar .boards-list {
- height: calc(100vh - #{$header-height} - #{$performance-bar-height});
+ height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height});
}
@@ -465,6 +494,6 @@ $new-sidebar-collapsed-width: 50px;
border-bottom-color: $active-border;
.badge {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index e5b467a2691..700be173039 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -117,13 +117,12 @@
}
.board-title {
- position: initial;
padding: 0;
border-bottom: 0;
> span {
display: block;
- transform: rotate(90deg) translate(25px, 0);
+ transform: rotate(90deg) translate(35px, 10px);
}
}
@@ -151,11 +150,18 @@
}
.board-header {
- border-top-left-radius: $border-radius-default;
- border-top-right-radius: $border-radius-default;
+ position: relative;
- &.has-border {
+ &.has-border::before {
border-top: 3px solid;
+ border-color: inherit;
+ border-top-left-radius: $border-radius-default;
+ border-top-right-radius: $border-radius-default;
+ content: '';
+ position: absolute;
+ width: calc(100% + 2px);
+ top: 0;
+ left: 0;
margin-top: -1px;
margin-right: -1px;
margin-left: -1px;
@@ -176,12 +182,16 @@
}
.board-title {
- position: relative;
margin: 0;
- padding: $gl-padding;
- padding-bottom: ($gl-padding + 3px);
+ padding: 12px $gl-padding;
font-size: 1em;
border-bottom: 1px solid $border-color;
+ display: flex;
+ align-items: center;
+}
+
+.board-title-text {
+ margin-right: auto;
}
.board-delete {
@@ -221,43 +231,10 @@
}
}
-.slide-down-enter {
- transform: translateY(-100%);
-}
-
-.slide-down-enter-active {
- transition: transform $fade-in-duration;
-
- + .board-list {
- transform: translateY(-136px);
- transition: none;
- }
-}
-
-.slide-down-enter-to {
- + .board-list {
- transform: translateY(0);
- transition: transform $fade-in-duration ease;
- }
-}
-
-.slide-down-leave {
- transform: translateY(0);
-}
-
-.slide-down-leave-active {
- transition: all $fade-in-duration;
- transform: translateY(-136px);
-
- + .board-list {
- transition: transform $fade-in-duration ease;
- transform: translateY(-136px);
- }
-}
-
.board-list-component {
height: calc(100% - 49px);
overflow: hidden;
+ position: relative;
}
.board-list {
@@ -429,7 +406,7 @@
}
.board-new-issue-form {
- z-index: 1;
+ z-index: 4;
margin: 5px;
}
@@ -440,6 +417,7 @@
&.right-sidebar {
top: 0;
bottom: 0;
+ height: 100%;
}
.issuable-sidebar-header {
@@ -471,7 +449,7 @@
padding-right: 35px;
> strong {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 486424fb729..50ec5110bf1 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -277,7 +277,7 @@
}
.trigger-build-variable {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
color: $code-color;
}
@@ -322,14 +322,13 @@
}
.build-dropdown {
- padding: $gl-padding 0;
+ @include new-style-dropdown;
- .dropdown-menu-toggle {
- margin-top: 8px;
- }
+ margin: $gl-padding 0;
+ padding: 0;
- .dropdown-menu {
- margin-top: -$gl-padding;
+ .dropdown-menu-toggle {
+ margin-top: #{$gl-padding / 2};
}
svg {
@@ -378,7 +377,7 @@
}
&.active {
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
.fa-arrow-right {
display: block;
diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
index 7b4eb689f1b..bf6a48889bf 100644
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ b/app/assets/stylesheets/pages/ci_projects.scss
@@ -22,7 +22,7 @@
vertical-align: middle !important;
a {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index cd9f2d787c5..994707422bb 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -141,17 +141,17 @@
display: inline-block;
background: $white-light;
color: $gl-text-color-secondary;
- padding: 0 5px;
+ padding: 0 4px;
cursor: pointer;
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
- font-size: $gl-font-size;
+ font-size: 12px;
line-height: $gl-font-size;
outline: none;
&.open {
- background: $gray-light;
+ background-color: darken($gray-light, 10%);
box-shadow: inset 0 0 2px rgba($black, 0.2);
}
@@ -213,7 +213,7 @@
.commit-sha {
font-size: 14px;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
}
@@ -226,6 +226,14 @@
vertical-align: baseline;
}
+ a.autodevops-badge {
+ color: $white-light;
+ }
+
+ a.autodevops-link {
+ color: $gl-link-color;
+ }
+
.commit-row-description {
font-size: 14px;
padding: 10px 15px;
@@ -286,6 +294,13 @@
.gpg-status-box {
+ padding: 2px 10px;
+ margin-right: $gl-padding;
+
+ &:empty {
+ display: none;
+ }
+
&.valid {
@include green-status-color;
}
@@ -299,7 +314,7 @@
.gpg-popover-status {
display: flex;
align-items: center;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
line-height: 1.5;
}
@@ -310,7 +325,6 @@
&.valid {
svg {
border: 1px solid $brand-success;
-
fill: $brand-success;
}
}
@@ -318,7 +332,6 @@
&.invalid {
svg {
border: 1px solid $common-gray-light;
-
fill: $common-gray-light;
}
}
diff --git a/app/assets/stylesheets/pages/convdev_index.scss b/app/assets/stylesheets/pages/convdev_index.scss
index 0413114c279..16702442f50 100644
--- a/app/assets/stylesheets/pages/convdev_index.scss
+++ b/app/assets/stylesheets/pages/convdev_index.scss
@@ -23,7 +23,7 @@ $space-between-cards: 8px;
line-height: 1;
color: $gl-text-color-secondary;
margin-left: 8px;
- font-weight: 500;
+ font-weight: $gl-font-weight-normal;
a {
font-size: 18px;
@@ -139,7 +139,7 @@ $space-between-cards: 8px;
.card-score-value {
font-size: 16px;
color: $gl-text-color;
- font-weight: 500;
+ font-weight: $gl-font-weight-normal;
}
.card-score-big {
@@ -147,7 +147,7 @@ $space-between-cards: 8px;
border-bottom: 1px solid $border-color;
font-size: 22px;
padding: 10px 0;
- font-weight: 500;
+ font-weight: $gl-font-weight-normal;
}
.card-buttons {
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 6753eb08285..2a92673d9fa 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -68,7 +68,7 @@
}
.stage-name {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
}
@@ -93,7 +93,7 @@
.header {
font-size: 30px;
line-height: 38px;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
margin: 0;
}
@@ -130,7 +130,7 @@
&.title {
line-height: 19px;
font-size: 14px;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
color: $gl-text-color;
}
@@ -211,7 +211,7 @@
box-shadow: inset 2px 0 0 0 $active-item-blue;
.stage-name {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
}
@@ -404,7 +404,7 @@
color: $gl-link-color;
line-height: 1.3;
vertical-align: top;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
.fa {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index da77346d8b2..a7acaf6c728 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -40,7 +40,7 @@
// "Changes suppressed. Click to show." link
.show-suppressed-diff {
font-size: 110%;
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
}
@@ -104,7 +104,7 @@
a {
float: left;
width: 35px;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
&[disabled] {
cursor: default;
@@ -395,7 +395,7 @@
background-color: transparent;
border: 0;
color: $gl-link-color;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
&:hover,
&:focus {
@@ -560,9 +560,13 @@
}
.diff-files-changed {
+ .inline-parallel-buttons {
+ position: relative;
+ z-index: 1;
+ }
+
.commit-stat-summary {
@include new-style-dropdown;
- z-index: -1;
@media (min-width: $screen-sm-min) {
margin-left: -$gl-padding;
@@ -574,10 +578,14 @@
@media (min-width: $screen-sm-min) {
position: -webkit-sticky;
position: sticky;
- top: 84px;
+ top: 24px;
background-color: $white-light;
z-index: 190;
+ &.diff-files-changed-merge-request {
+ top: 76px;
+ }
+
+ .files,
+ .alert {
margin-top: 1px;
@@ -606,6 +614,14 @@
}
}
+@media (min-width: $screen-sm-min) {
+ .with-performance-bar {
+ .diff-files-changed.diff-files-changed-merge-request {
+ top: 76px + $performance-bar-height;
+ }
+ }
+}
+
.diff-file-changes {
width: 450px;
z-index: 150;
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index f6b8c8ee2bc..d3cd4d507de 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -204,6 +204,8 @@
.gitlab-ci-yml-selector,
.dockerfile-selector,
.template-type-selector {
+ @include new-style-dropdown;
+
display: inline-block;
vertical-align: top;
font-family: $regular_font;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 00ebf4e26ac..9362d80d4e6 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -6,12 +6,14 @@
}
.environments-folder-name {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
padding-top: 20px;
}
.environments-container {
.ci-table {
+ @include new-style-dropdown;
+
.deployment-column {
> span {
word-break: break-all;
@@ -167,7 +169,7 @@
}
.metric-area {
- opacity: 0.8;
+ opacity: 0.25;
}
.prometheus-graph-overlay {
@@ -225,6 +227,26 @@
margin-top: 20px;
}
+.prometheus-graph-group {
+ display: flex;
+ flex-wrap: wrap;
+ padding: $gl-padding / 2;
+}
+
+.prometheus-graph {
+ flex: 1 0 auto;
+ min-width: 450px;
+ padding: $gl-padding / 2;
+
+ h5 {
+ font-size: 16px;
+ }
+
+ @media (max-width: $screen-sm-max) {
+ min-width: 100%;
+ }
+}
+
.prometheus-svg-container {
position: relative;
height: 0;
@@ -246,13 +268,19 @@
}
.text-metric-bold {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
+ }
+
+ .label-axis-text {
+ fill: $black;
+ font-weight: $gl-font-weight-normal;
+ font-size: 10px;
}
- .label-axis-text,
- .text-metric-usage {
+ .text-metric-usage,
+ .legend-metric-title {
fill: $black;
- font-weight: 500;
+ font-weight: $gl-font-weight-normal;
font-size: 12px;
}
@@ -289,9 +317,3 @@
}
}
}
-
-.prometheus-row {
- h5 {
- font-size: 16px;
- }
-}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 4c3fa1fb8d4..1723d716805 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -57,7 +57,7 @@
.event-title {
@include str-truncated(calc(100% - 174px));
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
color: $list-text-color;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index b78db402c13..d8a15faf7e9 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -8,13 +8,13 @@
.is-confidential {
color: $orange-600;
background-color: $orange-50;
- border-radius: 3px;
+ border-radius: $border-radius-default;
padding: 5px;
margin: 0 3px 0 -4px;
}
.is-not-confidential {
- border-radius: 3px;
+ border-radius: $border-radius-default;
padding: 5px;
margin: 0 3px 0 -4px;
}
@@ -35,7 +35,7 @@
.commit-box,
.info-well,
.commit-ci-menu,
- .files-changed,
+ .files-changed-inner,
.limited-header-width,
.limited-width-notes {
@extend .fixed-width-container;
@@ -81,6 +81,7 @@
border: 1px solid $white-normal;
padding: 5px;
max-height: calc(100vh - 100px);
+ max-width: 100%;
}
.emoji-block {
@@ -219,7 +220,7 @@
.right-sidebar {
position: absolute;
- top: $header-height;
+ top: $new-navbar-height;
bottom: 0;
right: 0;
transition: width .3s;
@@ -229,7 +230,7 @@
.issuable-sidebar {
width: calc(100% + 100px);
- height: calc(100% - #{$header-height});
+ height: calc(100% - #{$new-navbar-height});
overflow-y: scroll;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
@@ -259,7 +260,7 @@
padding-top: 10px;
}
- &:not(.issue-boards-sidebar):not([data-signed-in]) {
+ &:not(.issue-boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) {
.issuable-sidebar-header {
display: none;
}
@@ -270,7 +271,7 @@
}
.light {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
.no-value {
@@ -305,7 +306,7 @@
display: block;
margin-top: 4px;
font-size: 13px;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
.hide-expanded {
@@ -472,19 +473,37 @@
padding-top: 6px;
}
- .open .dropdown-menu {
+ .dropdown-menu {
width: 100%;
}
}
.with-performance-bar .right-sidebar {
- top: $header-height + $performance-bar-height;
+ top: $new-navbar-height + $performance-bar-height;
.issuable-sidebar {
- height: calc(100% - #{$header-height} - #{$performance-bar-height});
+ height: calc(100% - #{$new-navbar-height} - #{$performance-bar-height});
}
}
+.sidebar-move-issue-dropdown {
+ @include new-style-dropdown;
+}
+
+.sidebar-move-issue-confirmation-button {
+ width: 100%;
+
+ &.is-loading {
+ .sidebar-move-issue-confirmation-loading-icon {
+ display: inline-block;
+ }
+ }
+}
+
+.sidebar-move-issue-confirmation-loading-icon {
+ display: none;
+}
+
.detail-page-description {
padding: 16px 0;
@@ -497,6 +516,7 @@
color: $gray-darkest;
display: block;
margin: 16px 0 0;
+ font-size: 85%;
.author_link {
color: $gray-darkest;
@@ -597,6 +617,8 @@
}
.issuable-actions {
+ @include new-style-dropdown;
+
padding-top: 10px;
@media (min-width: $screen-sm-min) {
@@ -688,7 +710,7 @@
.issuable-info,
.task-status,
.issuable-updated-at {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
color: $gl-text-color-secondary;
a {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 8cdb3f34ae5..e8ca5cedaee 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -75,7 +75,7 @@ ul.related-merge-requests > li {
.merge-requests-title,
.related-branches-title {
font-size: 16px;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
.merge-request-id {
@@ -143,8 +143,12 @@ ul.related-merge-requests > li {
}
}
-.issue-form .select2-container {
- width: 250px !important;
+.issue-form {
+ @include new-style-dropdown;
+
+ .select2-container {
+ width: 250px !important;
+ }
}
.issues-footer {
@@ -186,6 +190,8 @@ ul.related-merge-requests > li {
}
.create-mr-dropdown-wrap {
+ @include new-style-dropdown;
+
.btn-group:not(.hide) {
display: flex;
}
@@ -212,15 +218,6 @@ ul.related-merge-requests > li {
}
li:not(.divider) {
- padding: 6px;
- cursor: pointer;
-
- &:hover,
- &:focus {
- background-color: $dropdown-hover-color;
- color: $white-light;
- }
-
&.droplab-item-selected {
.icon-container {
i {
@@ -244,12 +241,16 @@ ul.related-merge-requests > li {
strong {
display: block;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
}
}
}
+.discussion-reply-holder .note-edit-form {
+ display: block;
+}
+
@media (min-width: $screen-sm-min) {
.emoji-block .row {
display: flex;
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index ee48f7a3626..443f5500684 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -116,6 +116,8 @@
}
.manage-labels-list {
+ @include new-style-dropdown;
+
> li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light;
cursor: move;
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 3cbe8dededb..d4dc43035eb 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -22,7 +22,7 @@
}
h1:first-child {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
margin-bottom: 0.68em;
margin-top: 0;
font-size: 34px;
@@ -38,7 +38,7 @@
}
a {
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
}
@@ -54,7 +54,7 @@
padding: 15px;
.login-heading h3 {
- font-weight: 300;
+ font-weight: $gl-font-weight-normal;
line-height: 1.5;
margin: 0 0 10px;
}
@@ -186,7 +186,7 @@
}
label {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
.submit-container {
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index e7c07ef67f0..b3bab082a35 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -46,7 +46,7 @@
}
strong {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
}
@@ -55,6 +55,10 @@
display: -webkit-flex;
display: flex;
}
+
+ .dropdown-menu.dropdown-menu-align-right {
+ margin-top: -2px;
+ }
}
.form-horizontal {
@@ -96,6 +100,8 @@
}
.member-search-form {
+ @include new-style-dropdown;
+
position: relative;
@media (min-width: $screen-sm-min) {
@@ -221,7 +227,7 @@
}
.member {
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
overflow-wrap: break-word;
word-break: break-all;
}
@@ -304,3 +310,7 @@
}
}
}
+
+.member-form-control {
+ @include new-style-dropdown;
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 6bb013cca85..439636fe026 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -174,17 +174,6 @@
vertical-align: top;
}
- .mini-pipeline-graph-dropdown-menu .mini-pipeline-graph-dropdown-item {
- display: flex;
- align-items: center;
-
- .ci-status-text,
- .ci-status-icon {
- top: 0;
- margin-right: 10px;
- }
- }
-
.normal {
line-height: 28px;
}
@@ -197,7 +186,7 @@
@extend .ref-name;
color: $gl-text-color;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
overflow: hidden;
word-break: break-all;
@@ -228,7 +217,7 @@
.mr-widget-body {
h4 {
float: left;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
font-size: 14px;
line-height: inherit;
margin-top: 0;
@@ -239,7 +228,7 @@
}
time {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
}
@@ -249,7 +238,7 @@
}
label {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
.spacing {
@@ -257,12 +246,12 @@
}
.bold {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
color: $gl-gray-light;
}
.state-label {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
padding-right: 10px;
}
@@ -291,6 +280,7 @@
.dropdown-toggle {
.fa {
+ margin-left: 0;
color: inherit;
}
}
@@ -336,7 +326,7 @@
.text {
span {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
p {
@@ -489,6 +479,8 @@
}
.mr-source-target {
+ @include new-style-dropdown;
+
display: flex;
flex-wrap: wrap;
justify-content: space-between;
@@ -505,7 +497,7 @@
.panel-new-merge-request {
.panel-heading {
padding: 5px 10px;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
line-height: 25px;
}
@@ -610,6 +602,8 @@
}
.mr-version-controls {
+ @include new-style-dropdown;
+
position: relative;
background: $gray-light;
color: $gl-text-color;
@@ -651,7 +645,7 @@
}
.merge-request-tabs-holder {
- top: $header-height;
+ top: $new-navbar-height;
z-index: 200;
background-color: $white-light;
border-bottom: 1px solid $border-color;
@@ -681,7 +675,7 @@
}
.with-performance-bar .merge-request-tabs-holder {
- top: $header-height + $performance-bar-height;
+ top: $new-navbar-height + $performance-bar-height;
}
.merge-request-tabs {
@@ -727,3 +721,7 @@
font-size: 16px;
}
}
+
+.merge-request-form {
+ @include new-style-dropdown;
+}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 55e0ee1936e..32039936be7 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -7,7 +7,7 @@
padding: 10px 16px;
h4 {
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
.progress {
@@ -81,7 +81,7 @@
}
.remaining-days strong {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
.milestone-stat {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c90642178fc..5d7c85b16ef 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -20,13 +20,11 @@
}
}
-.new-note {
- display: none;
-}
-
.new-note,
.note-edit-form {
.note-form-actions {
+ @include new-style-dropdown;
+
position: relative;
margin: $gl-padding 0 0;
}
@@ -108,6 +106,7 @@
background-color: $orange-50;
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;
@@ -132,22 +131,9 @@
}
}
-.not-confidential {
- padding: 0;
- border-top: none;
-}
-
-.right-sidebar-expanded {
- .md-area {
- border-radius: 0;
- border-top: none;
- }
-}
-
-.right-sidebar-collapsed {
- .confidential-issue-warning {
- border-bottom: none;
- }
+.confidential-issue-warning + .md-area {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
}
.discussion-form {
@@ -200,7 +186,7 @@
.close {
color: $white-light;
opacity: 0.85;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
&:hover {
opacity: 1;
@@ -214,6 +200,10 @@
.discussion-reply-holder {
background-color: $white-light;
padding: 10px 16px;
+
+ &.is-replying {
+ padding-bottom: $gl-padding;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 2bb867052f6..e437bad4912 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -100,6 +100,20 @@ ul.notes {
}
}
+ .editing-spinner {
+ display: none;
+ }
+
+ &.is-requesting {
+ .note-timestamp {
+ display: none;
+ }
+
+ .editing-spinner {
+ display: inline-block;
+ }
+ }
+
&.is-editing {
.note-header,
.note-text,
@@ -365,9 +379,7 @@ ul.notes {
}
.discussion-header,
-.note-header {
- position: relative;
-
+.note-header-info {
a {
color: inherit;
@@ -402,6 +414,10 @@ ul.notes {
.note-header-info {
min-width: 0;
padding-bottom: 8px;
+
+ &.discussion {
+ padding-bottom: 0;
+ }
}
.system-note .note-header-info {
@@ -453,7 +469,12 @@ ul.notes {
}
.note-actions {
+ @include new-style-dropdown;
+
+ align-self: flex-start;
flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
// For PhantomJS that does not support flex
float: right;
margin-left: 10px;
@@ -463,18 +484,12 @@ ul.notes {
float: none;
margin-left: 0;
}
-
- .note-action-button {
- margin-left: 8px;
- }
-
- .more-actions-toggle {
- margin-left: 2px;
- }
}
.more-actions {
- display: inline-block;
+ float: right; // phantomjs fallback
+ display: flex;
+ align-items: flex-end;
.tooltip {
white-space: nowrap;
@@ -482,65 +497,57 @@ ul.notes {
}
.more-actions-toggle {
- padding: 0;
-
&:hover .icon,
&:focus .icon {
color: $blue-600;
}
-
- .icon {
- padding: 0 6px;
- }
}
.more-actions-dropdown {
width: 180px;
min-width: 180px;
- margin-top: $gl-btn-padding;
-
- li > a,
- li > .btn {
- color: $gl-text-color;
- padding: $gl-btn-padding;
- width: 100%;
- text-align: left;
-
- &:hover,
- &:focus {
- color: $gl-text-color;
- background-color: $blue-25;
- border-radius: $border-radius-default;
- }
- }
}
.discussion-actions {
@include notes-media('max', $screen-md-max) {
float: none;
margin-left: 0;
+ }
+}
- .note-action-button {
- margin-left: 0;
- }
+.note-actions-item {
+ margin-left: 12px;
+ display: flex;
+ align-items: center;
+
+ &.more-actions {
+ // compensate for narrow icon
+ margin-left: 10px;
}
}
.note-action-button {
- display: inline;
- line-height: 20px;
+ line-height: 1;
+ padding: 0;
+ min-width: 16px;
+ color: $gray-darkest;
.fa {
- color: $gray-darkest;
position: relative;
- font-size: 17px;
+ font-size: 16px;
}
+
+
svg {
height: 16px;
width: 16px;
- fill: $gray-darkest;
+ top: 0;
vertical-align: text-top;
+
+ path {
+ fill: currentColor;
+ }
}
.award-control-icon-positive,
@@ -613,18 +620,25 @@ ul.notes {
.note-role {
position: relative;
- top: -2px;
display: inline-block;
- padding-left: 7px;
- padding-right: 7px;
color: $notes-role-color;
font-size: 12px;
line-height: 20px;
- border: 1px solid $border-color;
- border-radius: $label-border-radius;
+ margin: 0 3px;
+
+ &.note-role-access {
+ padding: 0 7px;
+ border: 1px solid $border-color;
+ border-radius: $label-border-radius;
+ }
+
+ &.note-role-special {
+ text-shadow: 0 0 15px $gl-text-color-inverted;
+ }
}
+
/**
* Line note button on the side of diffs
*/
@@ -764,17 +778,25 @@ ul.notes {
background-color: transparent;
border: none;
outline: 0;
+ transition: color $general-hover-transition-duration $general-hover-transition-curve;
&.is-disabled {
cursor: default;
}
- &:not(.is-disabled):hover,
+ &:not(.is-disabled) {
+ &:hover,
+ &:focus {
+ color: $gl-text-green;
+ }
+ }
+
&.is-active {
color: $gl-text-green;
- svg {
- fill: $gl-text-green;
+ &:hover,
+ &:focus {
+ color: $gl-text-green-hover;
}
}
@@ -804,10 +826,6 @@ ul.notes {
}
}
-.discussion-notes .flash-container {
- margin-bottom: 0;
-}
-
// Merge request notes in diffs
.diff-file {
// Diff is inline
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index bdf07a99daf..c28b1e68008 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -14,3 +14,7 @@
font-size: 18px;
}
}
+
+.notification-form {
+ @include new-style-dropdown;
+}
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index dc1654e006e..7e2297c283f 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -12,7 +12,7 @@
.interval-pattern-form-group {
label {
margin-right: 10px;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
&[for='custom'] {
margin-right: 0;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 6185342b495..296b6310552 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -40,7 +40,7 @@
.btn.btn-retry:hover,
.btn.btn-retry:focus {
- border-color: $gray-darkest;
+ border-color: $dropdown-toggle-active-border-color;
background-color: $white-normal;
}
@@ -128,7 +128,7 @@
.branch-commit {
.ref-name {
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
max-width: 120px;
overflow: hidden;
display: inline-block;
@@ -202,12 +202,16 @@
.btn-group.open .dropdown-toggle {
box-shadow: none;
}
+
+ .pipeline-tags .label-container {
+ white-space: normal;
+ }
}
.stage-cell {
.mini-pipeline-graph-dropdown-toggle svg {
- height: 22px;
- width: 22px;
+ height: $ci-action-icon-size;
+ width: $ci-action-icon-size;
position: absolute;
top: -1px;
left: -1px;
@@ -219,7 +223,7 @@
display: inline-block;
position: relative;
vertical-align: middle;
- height: 22px;
+ height: $ci-action-icon-size;
margin: 3px 0;
+ .stage-container {
@@ -257,6 +261,8 @@
// Pipeline visualization
.pipeline-actions {
+ @include new-style-dropdown;
+
border-bottom: none;
}
@@ -272,7 +278,7 @@
.build-name {
float: right;
- font-weight: 500;
+ font-weight: $gl-font-weight-normal;
}
.ci-status-icon-failed svg {
@@ -281,7 +287,7 @@
.stage {
color: $gl-text-color-secondary;
- font-weight: 500;
+ font-weight: $gl-font-weight-normal;
vertical-align: middle;
}
}
@@ -308,7 +314,7 @@
a {
text-decoration: none;
- color: $gl-text-color-secondary;
+ color: $gl-text-color;
}
svg {
@@ -420,7 +426,7 @@
.stage-name {
margin: 0 0 15px 10px;
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
width: 176px;
white-space: nowrap;
overflow: hidden;
@@ -432,7 +438,11 @@
width: 186px;
margin-bottom: 10px;
white-space: normal;
- color: $gl-text-color-secondary;
+
+ // ensure .build-content has hover style when action-icon is hovered
+ .ci-job-dropdown-container:hover .build-content {
+ @extend .build-content:hover;
+ }
// Action Icons in big pipeline-graph nodes
.ci-action-icon-container .ci-action-icon-wrapper {
@@ -445,11 +455,11 @@
&:hover {
background-color: $stage-hover-bg;
- border: 1px solid $stage-hover-bg;
+ border: 1px solid $dropdown-toggle-active-border-color;
}
svg {
- fill: $border-color;
+ fill: $gl-text-color-secondary;
position: relative;
left: -1px;
top: -1px;
@@ -475,19 +485,10 @@
background-color: transparent;
border: none;
padding: 0;
- color: $gl-text-color-secondary;
&:focus {
outline: none;
}
-
- &:hover {
- color: $gl-text-color;
-
- .dropdown-counter-badge {
- color: $gl-text-color;
- }
- }
}
.build-content {
@@ -502,8 +503,7 @@
a.build-content:hover,
button.build-content:hover {
background-color: $stage-hover-bg;
- border: 1px solid $stage-hover-border;
- color: $gl-text-color;
+ border: 1px solid $dropdown-toggle-active-border-color;
}
@@ -564,7 +564,6 @@
// Triggers the dropdown in the big pipeline graph
.dropdown-counter-badge {
- color: $border-color;
font-weight: 100;
font-size: 15px;
position: absolute;
@@ -580,7 +579,7 @@
vertical-align: bottom;
display: inline-block;
position: relative;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
@mixin mini-pipeline-graph-color($color-light, $color-main, $color-dark) {
@@ -606,8 +605,8 @@ button.mini-pipeline-graph-dropdown-toggle {
background-color: $white-light;
border-width: 1px;
border-style: solid;
- width: 22px;
- height: 22px;
+ width: $ci-action-icon-size;
+ height: $ci-action-icon-size;
margin: 0;
padding: 0;
transition: all 0.2s linear;
@@ -669,105 +668,119 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
+@include new-style-dropdown('.big-pipeline-graph-dropdown-menu');
+@include new-style-dropdown('.mini-pipeline-graph-dropdown-menu');
+
// dropdown content for big and mini pipeline
.big-pipeline-graph-dropdown-menu,
.mini-pipeline-graph-dropdown-menu {
width: 195px;
max-width: 195px;
- li {
- padding: 2px 3px;
- }
-
.scrollable-menu {
padding: 0;
max-height: 245px;
overflow: auto;
}
- // Action icon on the right
- a.ci-action-icon-wrapper {
- color: $action-icon-color;
- border: 1px solid $action-icon-color;
- border-radius: 20px;
- width: 22px;
- height: 22px;
- padding: 2px 0 0 5px;
- cursor: pointer;
- float: right;
- margin: -26px 9px 0 0;
- font-size: 12px;
- background-color: $white-light;
+ li {
+ position: relative;
- &:hover,
- &:focus {
- background-color: $stage-hover-bg;
- border: 1px solid transparent;
+ // ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
+ &:hover > .mini-pipeline-graph-dropdown-item,
+ &:hover > .ci-job-component > .mini-pipeline-graph-dropdown-item {
+ @extend .mini-pipeline-graph-dropdown-item:hover;
}
- svg {
- width: 22px;
- height: 22px;
- left: -6px;
- position: relative;
- top: -3px;
- fill: $action-icon-color;
- }
+ // Action icon on the right
+ a.ci-action-icon-wrapper {
+ border-radius: 50%;
+ border: 1px solid $border-color;
+ width: $ci-action-icon-size;
+ height: $ci-action-icon-size;
+ padding: 2px 0 0 5px;
+ font-size: 12px;
+ background-color: $white-light;
+ position: absolute;
+ top: 50%;
+ right: $gl-padding;
+ margin-top: -#{$ci-action-icon-size / 2};
- &:hover svg,
- &:focus svg {
- fill: $gl-text-color;
- }
- }
+ &:hover,
+ &:focus {
+ background-color: $stage-hover-bg;
+ border: 1px solid $dropdown-toggle-active-border-color;
+ }
- // link to the build
- .mini-pipeline-graph-dropdown-item {
- padding: 3px 7px 4px;
- clear: both;
- font-weight: normal;
- line-height: 1.428571429;
- white-space: nowrap;
- margin: 0 5px;
- border-radius: 3px;
+ svg {
+ fill: $gl-text-color-secondary;
+ width: $ci-action-icon-size;
+ height: $ci-action-icon-size;
+ left: -6px;
+ position: relative;
+ top: -3px;
+ }
- // build name
- .ci-build-text,
- .ci-status-text {
- font-weight: 200;
- overflow: hidden;
+ &:hover svg,
+ &:focus svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ // link to the build
+ .mini-pipeline-graph-dropdown-item {
+ padding: 3px 7px 4px;
+ align-items: center;
+ clear: both;
+ display: flex;
+ font-weight: normal;
+ line-height: $line-height-base;
white-space: nowrap;
- text-overflow: ellipsis;
- max-width: 70%;
- color: $gl-text-color-secondary;
- margin-left: 2px;
- display: inline-block;
- top: 1px;
- vertical-align: text-bottom;
- position: relative;
+ border-radius: 3px;
- @media (max-width: $screen-xs-max) {
- max-width: 60%;
+ .ci-job-name-component {
+ align-items: center;
+ display: flex;
+ flex: 1;
}
- }
- // status icon on the left
- .ci-status-icon {
- top: 3px;
- position: relative;
+ // build name
+ .ci-build-text,
+ .ci-status-text {
+ font-weight: 200;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ max-width: 70%;
+ margin-left: 2px;
+ display: inline-block;
- > svg {
- overflow: visible;
- width: 18px;
- height: 18px;
+ @media (max-width: $screen-xs-max) {
+ max-width: 60%;
+ }
}
- }
- &:hover,
- &:focus {
- outline: none;
- text-decoration: none;
- color: $gl-text-color;
- background-color: $stage-hover-bg;
+ .ci-status-icon {
+ @extend .append-right-8;
+
+ position: relative;
+
+ > svg {
+ width: $pipeline-dropdown-status-icon-size;
+ height: $pipeline-dropdown-status-icon-size;
+ margin: 3px 0;
+ position: relative;
+ overflow: visible;
+ display: block;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ outline: none;
+ text-decoration: none;
+ background-color: $stage-hover-bg;
+ }
}
}
}
@@ -776,16 +789,9 @@ button.mini-pipeline-graph-dropdown-toggle {
.big-pipeline-graph-dropdown-menu {
width: 195px;
min-width: 195px;
- left: auto;
- right: -195px;
- top: -4px;
+ left: 100%;
+ top: -10px;
box-shadow: 0 1px 5px $black-transparent;
-
- .mini-pipeline-graph-dropdown-item {
- .ci-status-icon {
- top: -1px;
- }
- }
}
/**
@@ -806,15 +812,14 @@ button.mini-pipeline-graph-dropdown-toggle {
}
&::before {
- left: -5px;
- margin-top: -6px;
+ left: -6px;
+ margin-top: 3px;
border-width: 7px 5px 7px 0;
border-right-color: $border-color;
}
&::after {
- left: -4px;
- margin-top: -9px;
+ left: -5px;
border-width: 10px 7px 10px 0;
border-right-color: $white-light;
}
@@ -824,6 +829,7 @@ button.mini-pipeline-graph-dropdown-toggle {
* Top arrow in the dropdown in the mini pipeline graph
*/
.mini-pipeline-graph-dropdown-menu {
+ z-index: 200;
&::before,
&::after {
@@ -926,3 +932,12 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
}
+
+.pipelines-container .top-area .nav-controls > .btn:last-child {
+ float: none;
+}
+
+.autodevops-title {
+ font-weight: $gl-font-weight-normal;
+ line-height: 1.5;
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 14ad06b0ac2..c5d6ff66dd6 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -83,7 +83,7 @@
&::after {
content: "\00B7"; // Middle Dot
padding: 0 6px;
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
&:last-child {
@@ -277,7 +277,7 @@ table.u2f-registrations {
.oauth-application-show {
.scope-name {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
.scopes-list {
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index 305feaacaa1..c197494b152 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -1,3 +1,67 @@
+@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) {
+ .one {
+ background-color: $color-1;
+ border-top-left-radius: $border-radius-default;
+ }
+
+ .two {
+ background-color: $color-2;
+ border-top-right-radius: $border-radius-default;
+ }
+
+ .three {
+ background-color: $color-3;
+ border-bottom-left-radius: $border-radius-default;
+ }
+
+ .four {
+ background-color: $color-4;
+ border-bottom-right-radius: $border-radius-default;
+ }
+}
+
+.application-theme {
+ label {
+ margin-right: 20px;
+ text-align: center;
+ }
+
+ .preview {
+ font-size: 0;
+ margin-bottom: 10px;
+
+ &.indigo {
+ @include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500);
+ }
+
+ &.dark {
+ @include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600);
+ }
+
+ &.light {
+ @include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100);
+ }
+
+ &.blue {
+ @include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500);
+ }
+
+ &.green {
+ @include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500);
+ }
+ }
+
+ .preview-row {
+ display: block;
+ }
+
+ .quadrant {
+ display: inline-block;
+ height: 50px;
+ width: 80px;
+ }
+}
+
.syntax-theme {
label {
margin-right: 20px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 276465488e7..94e4f4334d4 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -2,7 +2,7 @@
margin: -16px;
.alert-link {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
}
@@ -10,41 +10,6 @@
.edit-project,
.import-project {
- .sharing-and-permissions {
- .header {
- padding-top: $gl-vert-padding;
- }
-
- .label-light {
- margin-bottom: 0;
- }
-
- .help-block {
- margin-top: 0;
- }
-
- .form-group {
- margin-bottom: 5px;
- }
-
- > .form-group {
- padding-left: 0;
- }
-
- select option[disabled] {
- display: none;
- }
- }
-
- select {
- transition: background 2s ease-out;
-
- &.highlight-changes {
- background: $highlight-changes-color;
- transition: none;
- }
- }
-
.help-block {
margin-bottom: 10px;
}
@@ -90,6 +55,162 @@
}
}
+.toggle-wrapper {
+ margin-top: 5px;
+}
+
+.project-feature-row > .toggle-wrapper {
+ margin: 10px 0;
+}
+
+.project-visibility-setting,
+.project-feature-settings {
+ border: 1px solid $border-color;
+ padding: 10px 32px;
+
+ @media (max-width: $screen-xs-min) {
+ padding: 10px 20px;
+ }
+}
+
+.project-visibility-setting .request-access {
+ line-height: 2;
+}
+
+.project-feature-settings {
+ background: $gray-lighter;
+ border-top: none;
+ margin-bottom: 16px;
+}
+
+.project-repo-select {
+ transition: background 2s ease-out;
+
+ &:disabled {
+ opacity: 0.75;
+ }
+
+ .highlight-changes & {
+ background: $highlight-changes-color;
+ transition: none;
+ }
+}
+
+.project-feature-controls {
+ display: flex;
+ align-items: center;
+ margin: 8px 0;
+ max-width: 432px;
+
+ .toggle-wrapper {
+ flex: 0;
+ margin-right: 10px;
+ }
+
+ .select-wrapper {
+ flex: 1;
+ }
+}
+
+.project-feature-setting-group {
+ padding-left: 32px;
+
+ .project-feature-controls {
+ max-width: 400px;
+ }
+
+ @media (max-width: $screen-xs-min) {
+ padding-left: 20px;
+ }
+}
+
+.project-feature-toggle {
+ position: relative;
+ border: none;
+ outline: 0;
+ display: block;
+ width: 100px;
+ height: 24px;
+ cursor: pointer;
+ user-select: none;
+ background: $feature-toggle-color-disabled;
+ border-radius: 12px;
+ padding: 3px;
+ transition: all .4s ease;
+
+ &::selection,
+ &::before::selection,
+ &::after::selection {
+ background: none;
+ }
+
+ &::before {
+ color: $feature-toggle-text-color;
+ font-size: 12px;
+ line-height: 24px;
+ position: absolute;
+ top: 0;
+ left: 25px;
+ right: 5px;
+ text-align: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ animation: animate-disabled .2s ease-in;
+ content: attr(data-disabled-text);
+ }
+
+ &::after {
+ position: relative;
+ display: block;
+ content: "";
+ width: 22px;
+ height: 18px;
+ left: 0;
+ border-radius: 9px;
+ background: $feature-toggle-color;
+ transition: all .2s ease;
+ }
+
+ &.checked {
+ background: $feature-toggle-color-enabled;
+
+ &::before {
+ left: 5px;
+ right: 25px;
+ animation: animate-enabled .2s ease-in;
+ content: attr(data-enabled-text);
+ }
+
+ &::after {
+ left: calc(100% - 22px);
+ }
+ }
+
+ &.disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+
+ @media (max-width: $screen-xs-min) {
+ width: 50px;
+
+ &::before,
+ &.checked::before {
+ display: none;
+ }
+ }
+
+ @keyframes animate-enabled {
+ 0%, 35% { opacity: 0; }
+ 100% { opacity: 1; }
+ }
+
+ @keyframes animate-disabled {
+ 0%, 35% { opacity: 0; }
+ 100% { opacity: 1; }
+ }
+}
+
.project-home-panel,
.group-home-panel {
padding-top: 24px;
@@ -114,7 +235,7 @@
margin-top: 10px;
margin-bottom: 10px;
font-size: 24px;
- font-weight: 400;
+ font-weight: $gl-font-weight-normal;
line-height: 1;
word-wrap: break-word;
@@ -259,7 +380,7 @@
border-width: 1px;
border-style: solid;
font-size: 13px;
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
line-height: 13px;
letter-spacing: .4px;
padding: 6px 14px;
@@ -299,28 +420,6 @@
}
}
-.project-visibility-level-holder {
- .radio {
- margin-bottom: 10px;
-
- i {
- margin: 2px 0;
- font-size: 20px;
- }
-
- .option-title {
- font-weight: normal;
- display: inline-block;
- color: $gl-text-color;
- }
-
- .option-descr {
- margin-left: 29px;
- color: $project-option-descr-color;
- }
- }
-}
-
.save-project-loader {
margin-top: 50px;
margin-bottom: 50px;
@@ -566,23 +665,23 @@ a.deploy-project-label {
&::before {
content: "OR";
position: absolute;
- left: 0;
- top: 40%;
+ left: -10px;
+ top: 50%;
z-index: 10;
padding: 8px 0;
text-align: center;
background-color: $white-light;
color: $gl-text-color-tertiary;
- transform: translateX(-50%);
+ transform: translateY(-50%);
font-size: 12px;
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
line-height: 20px;
// Mobile
@media (max-width: $screen-xs-max) {
left: 50%;
- top: 10px;
- transform: translateY(-50%);
+ top: 0;
+ transform: translateX(-50%);
padding: 0 8px;
}
}
@@ -822,11 +921,13 @@ pre.light-well {
}
}
-.new_protected_branch,
+.new-protected-branch,
.new-protected-tag {
+ @include new-style-dropdown;
+
label {
margin-top: 6px;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
}
@@ -843,19 +944,9 @@ pre.light-well {
.protected-branches-list,
.protected-tags-list {
- margin-bottom: 30px;
-
- a {
- color: $gl-text-color;
-
- &:hover {
- color: $gl-link-color;
- }
+ @include new-style-dropdown;
- &.is-active {
- font-weight: 600;
- }
- }
+ margin-bottom: 30px;
.settings-message {
margin: 0;
@@ -952,7 +1043,7 @@ pre.light-well {
&::before {
font-family: FontAwesome;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
font-style: normal;
}
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index ad17078c98a..69abb13add4 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -1,6 +1,6 @@
.fade-enter-active,
.fade-leave-active {
- transition: opacity .5s;
+ transition: opacity $sidebar-transition-duration;
}
.monaco-loader {
@@ -28,11 +28,6 @@
.project-refs-form,
.project-refs-target-form {
display: inline-block;
-
- &.disabled {
- opacity: 0.5;
- pointer-events: none;
- }
}
.fade-enter,
@@ -48,18 +43,32 @@
display: inline-block;
}
-.blob-viewer[data-type="rich"] {
- margin: 20px;
+@media (min-width: $screen-md-min) {
+ .blob-viewer[data-type="rich"] {
+ margin: 20px;
+ }
}
-.repository-view.tree-content-holder {
+.repository-view {
border: 1px solid $border-color;
border-radius: $border-radius-default;
color: $almost-black;
+ .tree-content-holder {
+ display: flex;
+ max-height: 100vh;
+ min-height: 300px;
+ }
+
+ .tree-content-holder-mini {
+ height: 100vh;
+ }
+
.panel-right {
- display: inline-block;
+ display: flex;
+ flex-direction: column;
width: 80%;
+ height: 100%;
.monaco-editor.vs {
.line-numbers {
@@ -90,16 +99,41 @@
}
.blob-viewer-container {
- height: calc(100vh - 63px);
+ flex: 1;
overflow: auto;
+
+ > div,
+ .file-content {
+ display: flex;
+ }
+
+ > div,
+ .file-content,
+ .blob-viewer,
+ .line-number,
+ .blob-content,
+ .code {
+ min-height: 100%;
+ width: 100%;
+ }
+
+ .line-numbers {
+ min-width: 44px;
+ }
+
+ .blob-content {
+ flex: 1;
+ overflow-x: auto;
+ }
}
#tabs {
+ flex-shrink: 0;
+ display: flex;
+ width: 100%;
padding-left: 0;
margin-bottom: 0;
- display: flex;
white-space: nowrap;
- width: 100%;
overflow-y: hidden;
overflow-x: auto;
@@ -114,6 +148,7 @@
border-right: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
white-space: nowrap;
+ cursor: pointer;
&.remove {
animation: swipeRightDissapear ease-in 0.1s;
@@ -133,10 +168,10 @@
a {
@include str-truncated(100px);
color: $black;
- display: inline-block;
width: 100px;
text-align: center;
vertical-align: middle;
+ text-decoration: none;
&.close {
width: auto;
@@ -146,15 +181,15 @@
}
}
- i.fa.fa-times,
- i.fa.fa-circle {
+ .close-icon,
+ .unsaved-icon {
float: right;
margin-top: 3px;
margin-left: 15px;
color: $gray-darkest;
}
- i.fa.fa-circle {
+ .unsaved-icon {
color: $brand-success;
}
@@ -173,7 +208,6 @@
padding: 5px 10px;
position: relative;
border-top: 1px solid $white-normal;
- margin-top: -5px;
}
#binary-viewer {
@@ -204,7 +238,7 @@
background: $gray-light;
padding: 20px;
- span.help-block {
+ .help-block {
padding-top: 7px;
margin-top: 0;
}
@@ -226,13 +260,12 @@
}
#sidebar {
+ flex: 1;
+ height: 100%;
&.sidebar-mini {
- display: inline-block;
- vertical-align: top;
width: 20%;
border-right: 1px solid $white-normal;
- height: calc(100vh + 20px);
overflow: auto;
}
@@ -259,9 +292,8 @@
display: inline-block;
font-size: 10px;
text-transform: uppercase;
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
color: $gray-darkest;
- width: 185px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -270,7 +302,7 @@
}
}
- .fa {
+ .file-icon {
margin-right: 5px;
}
@@ -280,118 +312,22 @@
}
a {
+ @include str-truncated(250px);
color: $almost-black;
display: inline-block;
vertical-align: middle;
}
-
- ul {
- list-style-type: none;
- padding: 0;
-
- li {
- border-bottom: 1px solid $border-gray-normal;
- padding: 10px 20px;
-
- a {
- color: $almost-black;
- }
-
- .fa {
- font-size: $code_font_size;
- margin-right: 5px;
- }
- }
- }
- }
-
-}
-
-.animation-container {
- background: $repo-editor-grey;
- height: 40px;
- overflow: hidden;
- position: relative;
-
- &.animation-container-small {
- height: 12px;
- }
-
- &::before {
- animation-duration: 1s;
- animation-fill-mode: forwards;
- animation-iteration-count: infinite;
- animation-name: blockTextShine;
- animation-timing-function: linear;
- background-image: $repo-editor-linear-gradient;
- background-repeat: no-repeat;
- background-size: 800px 45px;
- content: ' ';
- display: block;
- height: 100%;
- position: relative;
- }
-
- div {
- background: $white-light;
- height: 6px;
- left: 0;
- position: absolute;
- right: 0;
- }
-
- .line-of-code-1 {
- left: 0;
- top: 8px;
- }
-
- .line-of-code-2 {
- left: 150px;
- top: 0;
- height: 10px;
- }
-
- .line-of-code-3 {
- left: 0;
- top: 23px;
- }
-
- .line-of-code-4 {
- left: 0;
- top: 38px;
- }
-
- .line-of-code-5 {
- left: 200px;
- top: 28px;
- height: 10px;
- }
-
- .line-of-code-6 {
- top: 14px;
- left: 230px;
- height: 10px;
}
}
.render-error {
- min-height: calc(100vh - 63px);
+ min-height: calc(100vh - 62px);
p {
width: 100%;
}
}
-@keyframes blockTextShine {
- 0% {
- transform: translateX(-468px);
- }
-
- 100% {
- transform: translateX(468px);
- }
-}
-
@keyframes swipeRightAppear {
0% {
transform: scaleX(0.00);
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index 57c73295d1e..6cac37a4e28 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -30,7 +30,7 @@
}
h4 {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index b9818ffcf42..13dd7b5a780 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -94,7 +94,7 @@ input[type="checkbox"]:hover {
&::before {
font-family: FontAwesome;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
font-style: normal;
}
}
@@ -166,7 +166,7 @@ input[type="checkbox"]:hover {
.dropdown-menu {
transition-duration: 100ms, 75ms;
transition-delay: 75ms, 100ms;
- transform: translateY(13px);
+ transform: translateY(7px);
opacity: 1;
}
}
@@ -190,6 +190,8 @@ input[type="checkbox"]:hover {
}
.search-holder {
+ @include new-style-dropdown;
+
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
display: flex;
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 15df51e9c69..41a6ba2023a 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -143,6 +143,47 @@
}
}
+.visibility-level-setting {
+ .radio {
+ margin-bottom: 10px;
+
+ i.fa {
+ margin: 2px 0;
+ font-size: 20px;
+ }
+
+ .option-title {
+ font-weight: $gl-font-weight-normal;
+ display: inline-block;
+ color: $gl-text-color;
+ }
+
+ .option-description,
+ .option-disabled-reason {
+ margin-left: 29px;
+ color: $project-option-descr-color;
+ }
+
+ .option-disabled-reason {
+ display: none;
+ }
+
+ &.disabled {
+ i.fa {
+ opacity: 0.5;
+ }
+
+ .option-description {
+ display: none;
+ }
+
+ .option-disabled-reason {
+ display: block;
+ }
+ }
+ }
+}
+
.prometheus-metrics-monitoring {
.panel {
.panel-toggle {
diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss
index 23a9c2ada80..bfe065dbbaf 100644
--- a/app/assets/stylesheets/pages/sherlock.scss
+++ b/app/assets/stylesheets/pages/sherlock.scss
@@ -29,5 +29,5 @@ table .sherlock-code {
.sherlock-line-samples-table .slow {
color: $red-500;
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index d7a9dda3770..6c8d87185e9 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -108,14 +108,14 @@
margin: 0;
float: none;
display: inline-block;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
padding: 0 5px;
line-height: inherit;
font-size: 14px;
}
.action-name {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
}
.todo-body {
@@ -262,6 +262,10 @@
}
a {
- font-weight: 600;
+ font-weight: $gl-font-weight-bold;
}
}
+
+.todos-filters {
+ @include new-style-dropdown;
+}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index a8e0f251cd3..224eee90a3f 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -29,6 +29,10 @@
margin-right: 15px;
}
+ .tree-ref-target-holder {
+ display: inline-block;
+ }
+
.repo-breadcrumb {
li:last-of-type {
position: relative;
@@ -216,6 +220,9 @@
}
.blob-upload-dropzone-previews {
+ display: flex;
+ justify-content: center;
+ align-items: center;
text-align: center;
border: 2px;
border-style: dashed;
@@ -224,7 +231,7 @@
}
.upload-link {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
color: $md-link-color;
}
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index 798e060a261..48ac5b21db8 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -1,7 +1,7 @@
.gitlab-ui-dev-kit {
> h2 {
margin: 35px 0 20px;
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
.example {
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index fa6bdd297eb..b7d4e7bf582 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -37,7 +37,7 @@
}
.light {
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
color: $gl-text-color-secondary;
}
@@ -89,7 +89,7 @@
h3 {
font-size: 19px;
- font-weight: normal;
+ font-weight: $gl-font-weight-normal;
margin: $gl-padding 0;
}
}
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index b085c56390d..c7297a34ad8 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -281,7 +281,7 @@
$xterm-fg-255: #eee;
.term-bold {
- font-weight: bold;
+ font-weight: $gl-font-weight-bold;
}
.term-italic {
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index 113e6e86bb5..b07a5ae22cd 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -17,7 +17,7 @@
.wiki h3 {
font-size: 18px;
- font-weight: bold;
+ font-weight: 600;
}
header,
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index 4b0ec54b3f4..92df1c8dff0 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -45,7 +45,7 @@ class Admin::AppearancesController < Admin::ApplicationController
# Use callbacks to share common setup or constraints between actions.
def set_appearance
- @appearance = Appearance.last || Appearance.new
+ @appearance = Appearance.current || Appearance.new
end
# Only allow a trusted parameter "white list" through.
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8367c22d1ca..4dfb397e82c 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -20,8 +20,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def usage_data
respond_to do |format|
format.html do
- usage_data = Gitlab::UsageData.data
- usage_data_json = params[:pretty] ? JSON.pretty_generate(usage_data) : usage_data.to_json
+ usage_data_json = JSON.pretty_generate(Gitlab::UsageData.data)
render html: Gitlab::Highlight.highlight('payload.json', usage_data_json)
end
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 762e36ee2e9..c49b6459452 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -2,7 +2,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
before_action :finder, only: [:edit, :update, :destroy]
def index
- @broadcast_messages = BroadcastMessage.reorder("ends_at DESC").page(params[:page])
+ @broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page])
@broadcast_message = BroadcastMessage.new
end
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 05e749c00c0..e85cdcb8db7 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,7 +1,7 @@
class Admin::DashboardController < Admin::ApplicationController
def index
- @projects = Project.without_deleted.with_route.limit(10)
- @users = User.limit(10)
- @groups = Group.with_route.limit(10)
+ @projects = Project.order_id_desc.without_deleted.with_route.limit(10)
+ @users = User.order_id_desc.limit(10)
+ @groups = Group.order_id_desc.with_route.limit(10)
end
end
diff --git a/app/controllers/admin/logs_controller.rb b/app/controllers/admin/logs_controller.rb
index b999018dde4..12a27cede75 100644
--- a/app/controllers/admin/logs_controller.rb
+++ b/app/controllers/admin/logs_controller.rb
@@ -1,2 +1,18 @@
class Admin::LogsController < Admin::ApplicationController
+ before_action :loggers
+
+ def show
+ end
+
+ private
+
+ def loggers
+ @loggers ||= [
+ Gitlab::AppLogger,
+ Gitlab::GitLogger,
+ Gitlab::EnvironmentLogger,
+ Gitlab::SidekiqLogger,
+ Gitlab::RepositoryCheckLogger
+ ]
+ end
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 0b6cd71e651..50cf2643390 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -3,9 +3,9 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer]
def index
- finder = Admin::ProjectsFinder.new(params: params, current_user: current_user)
- @projects = finder.execute
- @sort = finder.sort
+ params[:sort] ||= 'latest_activity_desc'
+ @sort = params[:sort]
+ @projects = Admin::ProjectsFinder.new(params: params, current_user: current_user).execute
respond_to do |format|
format.html
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index fa1bc72560e..cbcef70e957 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -17,7 +17,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def keys
- @keys = user.keys
+ @keys = user.keys.order_id_desc
end
def new
@@ -117,11 +117,14 @@ class Admin::UsersController < Admin::ApplicationController
user_params_with_pass = user_params.dup
if params[:user][:password].present?
- user_params_with_pass.merge!(
+ password_params = {
password: params[:user][:password],
- password_confirmation: params[:user][:password_confirmation],
- password_expires_at: Time.now
- )
+ password_confirmation: params[:user][:password_confirmation]
+ }
+
+ password_params[:password_expires_at] = Time.now unless changing_own_password?
+
+ user_params_with_pass.merge!(password_params)
end
respond_to do |format|
@@ -167,6 +170,10 @@ class Admin::UsersController < Admin::ApplicationController
protected
+ def changing_own_password?
+ user == current_user
+ end
+
def user
@user ||= User.find_by!(username: params[:id])
end
@@ -204,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController
:provider,
:remember_me,
:skype,
+ :theme_id,
:twitter,
:username,
:website_url
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 5b448008a1b..97922e39ba8 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -117,7 +117,7 @@ class ApplicationController < ActionController::Base
Raven.capture_exception(exception) if sentry_enabled?
application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace
- application_trace.map!{ |t| " #{t}\n" }
+ application_trace.map! { |t| " #{t}\n" }
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
end
@@ -202,7 +202,7 @@ class ApplicationController < ActionController::Base
end
def check_password_expiration
- if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && current_user.allow_password_authentication?
+ if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user?
return redirect_to new_profile_password_path
end
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 3120916c5bb..dfc8bd0ba81 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -1,5 +1,7 @@
class AutocompleteController < ApplicationController
- skip_before_action :authenticate_user!, only: [:users]
+ AWARD_EMOJI_MAX = 100
+
+ skip_before_action :authenticate_user!, only: [:users, :award_emojis]
before_action :load_project, only: [:users]
before_action :find_users, only: [:users]
@@ -39,15 +41,23 @@ class AutocompleteController < ApplicationController
project = Project.find_by_id(params[:project_id])
projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id])
- no_project = {
- id: 0,
- name_with_namespace: 'No project'
- }
- projects.unshift(no_project) unless params[:offset_id].present?
-
render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
end
+ def award_emojis
+ emoji_with_count = AwardEmoji
+ .limit(AWARD_EMOJI_MAX)
+ .where(user: current_user)
+ .group(:name)
+ .order('count_all DESC, name ASC')
+ .count
+
+ # Transform from hash to array to guarantee json order
+ # e.g. { 'thumbsup' => 2, 'thumbsdown' = 1 }
+ # => [{ name: 'thumbsup' }, { name: 'thumbsdown' }]
+ render json: emoji_with_count.map { |k, v| { name: k } }
+ end
+
private
def find_users
diff --git a/app/controllers/boards/application_controller.rb b/app/controllers/boards/application_controller.rb
new file mode 100644
index 00000000000..b2675025fc0
--- /dev/null
+++ b/app/controllers/boards/application_controller.rb
@@ -0,0 +1,21 @@
+module Boards
+ class ApplicationController < ::ApplicationController
+ respond_to :json
+
+ rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
+
+ private
+
+ def board
+ @board ||= Board.find(params[:board_id])
+ end
+
+ def board_parent
+ @board_parent ||= board.parent
+ end
+
+ def record_not_found(exception)
+ render json: { error: exception.message }, status: :not_found
+ end
+ end
+end
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
new file mode 100644
index 00000000000..8d4ec2d6d9d
--- /dev/null
+++ b/app/controllers/boards/issues_controller.rb
@@ -0,0 +1,90 @@
+module Boards
+ class IssuesController < Boards::ApplicationController
+ include BoardsResponses
+
+ before_action :authorize_read_issue, only: [:index]
+ before_action :authorize_create_issue, only: [:create]
+ before_action :authorize_update_issue, only: [:update]
+ skip_before_action :authenticate_user!, only: [:index]
+
+ 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)
+
+ render json: {
+ issues: serialize_as_json(issues.preload(:project)),
+ size: issues.total_count
+ }
+ end
+
+ def create
+ service = Boards::Issues::CreateService.new(board_parent, project, current_user, issue_params)
+ issue = service.execute
+
+ if issue.valid?
+ render json: serialize_as_json(issue)
+ else
+ render json: issue.errors, status: :unprocessable_entity
+ end
+ end
+
+ def update
+ service = Boards::Issues::MoveService.new(board_parent, current_user, move_params)
+
+ if service.execute(issue)
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ private
+
+ def make_sure_position_is_set(issues)
+ issues.each do |issue|
+ issue.move_to_end && issue.save unless issue.relative_position
+ end
+ end
+
+ def issue
+ @issue ||= issues_finder.execute.find(params[:id])
+ end
+
+ def filter_params
+ params.merge(board_id: params[:board_id], id: params[:list_id])
+ .reject { |_, value| value.nil? }
+ end
+
+ def issues_finder
+ IssuesFinder.new(current_user, project_id: board_parent.id)
+ end
+
+ def project
+ board_parent
+ end
+
+ def move_params
+ params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_id, :move_after_id)
+ end
+
+ def issue_params
+ params.require(:issue)
+ .permit(:title, :milestone_id, :project_id)
+ .merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
+ end
+
+ def serialize_as_json(resource)
+ resource.as_json(
+ labels: true,
+ only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position],
+ include: {
+ project: { only: [:id, :path] },
+ assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
+ milestone: { only: [:id, :title] }
+ },
+ user: current_user
+ )
+ end
+ end
+end
diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb
new file mode 100644
index 00000000000..381fd4d7508
--- /dev/null
+++ b/app/controllers/boards/lists_controller.rb
@@ -0,0 +1,75 @@
+module Boards
+ class ListsController < Boards::ApplicationController
+ include BoardsResponses
+
+ before_action :authorize_admin_list, only: [:create, :update, :destroy, :generate]
+ before_action :authorize_read_list, only: [:index]
+ skip_before_action :authenticate_user!, only: [:index]
+
+ def index
+ lists = Boards::Lists::ListService.new(board.parent, current_user).execute(board)
+
+ render json: serialize_as_json(lists)
+ end
+
+ def create
+ list = Boards::Lists::CreateService.new(board.parent, current_user, list_params).execute(board)
+
+ if list.valid?
+ render json: serialize_as_json(list)
+ else
+ render json: list.errors, status: :unprocessable_entity
+ end
+ end
+
+ def update
+ list = board.lists.movable.find(params[:id])
+ service = Boards::Lists::MoveService.new(board_parent, current_user, move_params)
+
+ if service.execute(list)
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def destroy
+ list = board.lists.destroyable.find(params[:id])
+ service = Boards::Lists::DestroyService.new(board_parent, current_user)
+
+ if service.execute(list)
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def generate
+ service = Boards::Lists::GenerateService.new(board_parent, current_user)
+
+ if service.execute(board)
+ render json: serialize_as_json(board.lists.movable)
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ private
+
+ def list_params
+ params.require(:list).permit(:label_id)
+ end
+
+ def move_params
+ params.require(:list).permit(:position)
+ end
+
+ def serialize_as_json(resource)
+ resource.as_json(
+ only: [:id, :list_type, :position],
+ methods: [:title],
+ label: true
+ )
+ end
+ end
+end
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index ea441b1736b..b75e401a8df 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -69,7 +69,7 @@ module AuthenticatesWithTwoFactor
if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
# Remove any lingering user data from login
session.delete(:otp_user_id)
- session.delete(:challenges)
+ session.delete(:challenge)
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb
new file mode 100644
index 00000000000..2c9c095a5d7
--- /dev/null
+++ b/app/controllers/concerns/boards_responses.rb
@@ -0,0 +1,42 @@
+module BoardsResponses
+ def authorize_read_list
+ authorize_action_for!(board.parent, :read_list)
+ end
+
+ def authorize_read_issue
+ authorize_action_for!(board.parent, :read_issue)
+ end
+
+ def authorize_update_issue
+ authorize_action_for!(issue, :admin_issue)
+ end
+
+ def authorize_create_issue
+ authorize_action_for!(project, :admin_issue)
+ end
+
+ def authorize_admin_list
+ authorize_action_for!(board.parent, :admin_list)
+ end
+
+ def authorize_action_for!(resource, ability)
+ return render_403 unless can?(current_user, ability, resource)
+ end
+
+ def respond_with_boards
+ respond_with(@boards)
+ end
+
+ def respond_with_board
+ respond_with(@board)
+ end
+
+ def respond_with(resource)
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: serialize_as_json(resource)
+ end
+ end
+ end
+end
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
index 52e06f4945a..1ab107168c0 100644
--- a/app/controllers/concerns/cycle_analytics_params.rb
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -6,6 +6,13 @@ module CycleAnalyticsParams
end
def start_date(params)
- params[:start_date] == '30' ? 30.days.ago : 90.days.ago
+ case params[:start_date]
+ when '7'
+ 7.days.ago
+ when '30'
+ 30.days.ago
+ else
+ 90.days.ago
+ end
end
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 0c3b68a7ac3..4079072a930 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -10,7 +10,7 @@ module IssuableActions
def destroy
issuable.destroy
destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
- TodoService.new.public_send(destroy_method, issuable, current_user)
+ TodoService.new.public_send(destroy_method, issuable, current_user) # rubocop:disable GitlabSecurity/PublicSend
name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted."
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index b43b2c5621f..23909bd2d39 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -15,7 +15,17 @@ module IssuableCollections
end
def merge_requests_collection
- merge_requests_finder.execute.preload(:source_project, :target_project, :author, :assignee, :labels, :milestone, :head_pipeline, target_project: :namespace, merge_request_diff: :merge_request_diff_commits)
+ merge_requests_finder.execute.preload(
+ :source_project,
+ :target_project,
+ :author,
+ :assignee,
+ :labels,
+ :milestone,
+ head_pipeline: :project,
+ target_project: :namespace,
+ merge_request_diff: :merge_request_diff_commits
+ )
end
def issues_finder
@@ -26,6 +36,34 @@ module IssuableCollections
@merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder)
end
+ def redirect_out_of_range(relation, total_pages)
+ return false if total_pages.zero?
+
+ out_of_range = relation.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
+
+ def issues_page_count(relation)
+ page_count_for_relation(relation, issues_finder.row_count)
+ end
+
+ def merge_requests_page_count(relation)
+ page_count_for_relation(relation, merge_requests_finder.row_count)
+ end
+
+ def page_count_for_relation(relation, row_count)
+ limit = relation.limit_value.to_f
+
+ return 1 if limit.zero?
+
+ (row_count.to_f / limit).ceil
+ end
+
def issuable_finder_for(finder_class)
finder_class.new(current_user, filter_params)
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index af5f683bab5..18fd8eb114d 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -3,6 +3,7 @@ module NotesActions
extend ActiveSupport::Concern
included do
+ before_action :set_polling_interval_header, only: [:index]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :note_project, only: [:create]
end
@@ -12,14 +13,18 @@ module NotesActions
notes_json = { notes: [], last_fetched_at: current_fetched_at }
- @notes = notes_finder.execute.inc_relations_for_view
- @notes = prepare_notes_for_rendering(@notes)
+ notes = notes_finder.execute
+ .inc_relations_for_view
+ .reject { |n| n.cross_reference_not_visible_for?(current_user) }
- @notes.each do |note|
- next if note.cross_reference_not_visible_for?(current_user)
+ notes = prepare_notes_for_rendering(notes)
- notes_json[:notes] << note_json(note)
- end
+ notes_json[:notes] =
+ if noteable.discussions_rendered_on_frontend?
+ note_serializer.represent(notes)
+ else
+ notes.map { |note| note_json(note) }
+ end
render json: notes_json
end
@@ -82,22 +87,27 @@ module NotesActions
}
if note.persisted?
- attrs.merge!(
- valid: true,
- id: note.id,
- discussion_id: note.discussion_id(noteable),
- html: note_html(note),
- note: note.note
- )
+ attrs[:valid] = true
- discussion = note.to_discussion(noteable)
- unless discussion.individual_note?
+ if noteable.nil? || noteable.discussions_rendered_on_frontend?
+ attrs.merge!(note_serializer.represent(note))
+ else
attrs.merge!(
- discussion_resolvable: discussion.resolvable?,
-
- diff_discussion_html: diff_discussion_html(discussion),
- discussion_html: discussion_html(discussion)
+ id: note.id,
+ discussion_id: note.discussion_id(noteable),
+ html: note_html(note),
+ note: note.note
)
+
+ discussion = note.to_discussion(noteable)
+ unless discussion.individual_note?
+ attrs.merge!(
+ discussion_resolvable: discussion.resolvable?,
+
+ diff_discussion_html: diff_discussion_html(discussion),
+ discussion_html: discussion_html(discussion)
+ )
+ end
end
else
attrs.merge!(
@@ -168,6 +178,10 @@ module NotesActions
)
end
+ def set_polling_interval_header
+ Gitlab::PollingInterval.set_header(response, interval: 6_000)
+ end
+
def noteable
@noteable ||= notes_finder.target
end
@@ -180,6 +194,10 @@ module NotesActions
@notes_finder ||= NotesFinder.new(project, current_user, finder_params)
end
+ def note_serializer
+ NoteSerializer.new(project: project, noteable: noteable, current_user: current_user)
+ end
+
def note_project
return @note_project if defined?(@note_project)
return nil unless project
diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb
new file mode 100644
index 00000000000..bb2c1dfa00a
--- /dev/null
+++ b/app/controllers/concerns/renders_commits.rb
@@ -0,0 +1,7 @@
+module RendersCommits
+ def prepare_commits_for_rendering(commits)
+ Banzai::CommitRenderer.render(commits, @project, current_user)
+
+ commits
+ end
+end
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
index 41c3114ad1e..4791bc561a4 100644
--- a/app/controllers/concerns/renders_notes.rb
+++ b/app/controllers/concerns/renders_notes.rb
@@ -1,7 +1,8 @@
module RendersNotes
- def prepare_notes_for_rendering(notes)
+ def prepare_notes_for_rendering(notes, noteable = nil)
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
+ preload_first_time_contribution_for_authors(noteable, notes)
Banzai::NoteRenderer.render(notes, @project, current_user)
notes
@@ -19,4 +20,10 @@ module RendersNotes
def preload_noteable_for_regular_notes(notes)
ActiveRecord::Associations::Preloader.new.preload(notes.reject(&:for_commit?), :noteable)
end
+
+ def preload_first_time_contribution_for_authors(noteable, notes)
+ return unless noteable.is_a?(Issuable) && noteable.first_contribution?
+
+ notes.each {|n| n.specialize_for_first_contribution!(noteable)}
+ end
end
diff --git a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
index ad2f4bbc486..0218ac83441 100644
--- a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
+++ b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb
@@ -1,5 +1,8 @@
module RequiresWhitelistedMonitoringClient
extend ActiveSupport::Concern
+
+ include Gitlab::CurrentSettings
+
included do
before_action :validate_ip_whitelisted_or_valid_token!
end
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 742157d113d..8057a0b455c 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,5 +1,7 @@
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
+ @sort = params[:sort] || 'id_desc'
+
@groups =
if params[:parent_id] && Group.supports_nested_groups?
parent = Group.find_by(id: params[:parent_id])
@@ -15,7 +17,7 @@ class Dashboard::GroupsController < Dashboard::ApplicationController
@groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
@groups = @groups.includes(:route)
- @groups = @groups.sort(@sort = params[:sort])
+ @groups = @groups.sort(@sort)
@groups = @groups.page(params[:page])
respond_to do |format|
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 74fe45e1ff6..f71ab702e71 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -52,8 +52,10 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def load_events
- @events = Event.in_projects(load_projects(params.merge(non_public: true)))
- @events = event_filter.apply_filter(@events).with_associations
- @events = @events.limit(20).offset(params[:offset] || 0)
+ projects = load_projects(params.merge(non_public: true))
+
+ @events = EventCollection
+ .new(projects, offset: params[:offset].to_i, filter: event_filter)
+ .to_a
end
end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index f9c31920302..19a5db6fd17 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -29,9 +29,9 @@ class DashboardController < Dashboard::ApplicationController
current_user.authorized_projects
end
- @events = Event.in_projects(projects)
- @events = @event_filter.apply_filter(@events).with_associations
- @events = @events.limit(20).offset(params[:offset] || 0)
+ @events = EventCollection
+ .new(projects, offset: params[:offset].to_i, filter: @event_filter)
+ .to_a
end
def set_show_full_reference
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 741879dee35..762c6ebf3a3 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -6,7 +6,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def index
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
- @projects = load_projects.page(params[:page])
+ @projects = load_projects
respond_to do |format|
format.html
@@ -21,7 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
def trending
params[:trending] = true
@sort = params[:sort]
- @projects = load_projects.page(params[:page])
+ @projects = load_projects
respond_to do |format|
format.html
@@ -34,7 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def starred
- @projects = load_projects.reorder('star_count DESC').page(params[:page])
+ @projects = load_projects.reorder('star_count DESC')
respond_to do |format|
format.html
@@ -50,6 +50,9 @@ class Explore::ProjectsController < Explore::ApplicationController
def load_projects
ProjectsFinder.new(current_user: current_user, params: params)
- .execute.includes(:route, namespace: :route)
+ .execute
+ .includes(:route, namespace: :route)
+ .page(params[:page])
+ .without_count
end
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index c0ac47e363d..96ce686c989 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -34,7 +34,7 @@ class Groups::ApplicationController < ApplicationController
def build_canonical_path(group)
params[:group_id] = group.to_param
-
+
url_for(params)
end
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 5c10d7bc261..7a7bcb1a3d2 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -35,13 +35,13 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def edit
- render_404 if @milestone.is_legacy_group_milestone?
+ render_404 if @milestone.legacy_group_milestone?
end
def update
# Keep this compatible with legacy group milestones where we have to update
# all projects milestones states at once.
- if @milestone.is_legacy_group_milestone?
+ if @milestone.legacy_group_milestone?
update_params = milestone_params.select { |key| key == "state_event" }
milestones = @milestone.milestones
else
@@ -67,7 +67,7 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def milestone_path
- if @milestone.is_legacy_group_milestone?
+ if @milestone.legacy_group_milestone?
group_milestone_path(group, @milestone.safe_title, title: @milestone.title)
else
group_milestone_path(group, @milestone.iid)
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 27137ffde54..3769a2cde33 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -10,7 +10,7 @@ class GroupsController < Groups::ApplicationController
# Authorize
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
- before_action :authorize_create_group!, only: [:new, :create]
+ before_action :authorize_create_group!, only: [:new]
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
before_action :group_merge_requests, only: [:merge_requests]
@@ -25,7 +25,7 @@ class GroupsController < Groups::ApplicationController
end
def new
- @group = Group.new
+ @group = Group.new(params.permit(:parent_id))
end
def create
@@ -121,9 +121,14 @@ class GroupsController < Groups::ApplicationController
end
def authorize_create_group!
- unless can?(current_user, :create_group)
- return render_404
- end
+ allowed = if params[:parent_id].present?
+ parent = Group.find_by(id: params[:parent_id])
+ can?(current_user, :create_subgroup, parent)
+ else
+ can?(current_user, :create_group)
+ end
+
+ render_404 unless allowed
end
def determine_layout
@@ -160,9 +165,9 @@ class GroupsController < Groups::ApplicationController
end
def load_events
- @events = Event.in_projects(@projects)
- @events = event_filter.apply_filter(@events).with_associations
- @events = @events.limit(20).offset(params[:offset] || 0)
+ @events = EventCollection
+ .new(@projects, offset: params[:offset].to_i, filter: event_filter)
+ .to_a
end
def user_actions
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 53a5981e564..ab18d86dcae 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -64,19 +64,19 @@ class Import::GithubController < Import::BaseController
end
def import_enabled?
- __send__("#{provider}_import_enabled?")
+ __send__("#{provider}_import_enabled?") # rubocop:disable GitlabSecurity/PublicSend
end
def new_import_url
- public_send("new_import_#{provider}_url")
+ public_send("new_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend
end
def status_import_url
- public_send("status_import_#{provider}_url")
+ public_send("status_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend
end
def callback_import_url
- public_send("callback_import_#{provider}_url")
+ public_send("callback_import_#{provider}_url") # rubocop:disable GitlabSecurity/PublicSend
end
def provider_unauthorized
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index 73837ffbe67..407154e59a0 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -15,7 +15,7 @@ class Import::GitlabController < Import::BaseController
@already_added_projects = current_user.created_projects.where(import_type: "gitlab")
already_added_projects_names = @already_added_projects.pluck(:import_source)
- @repos = @repos.to_a.reject{ |repo| already_added_projects_names.include? repo["path_with_namespace"] }
+ @repos = @repos.to_a.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] }
end
def jobs
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index b4213574561..9612b8d8514 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -10,6 +10,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
end
+ if Gitlab::LDAP::Config.enabled?
+ Gitlab::LDAP::Config.available_servers.each do |server|
+ define_method server['provider_name'] do
+ ldap
+ end
+ end
+ end
+
# Extend the standard message generation to accept our custom exception
def failure_message
exception = env["omniauth.error"]
@@ -142,13 +150,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def oauth
@oauth ||= request.env['omniauth.auth']
end
-
+
def fail_login
error_message = @user.errors.full_messages.to_sentence
return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
end
-
+
def fail_ldap_login
flash[:alert] = 'Access denied for your LDAP account.'
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index aa8cf630032..fda944adecd 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -1,8 +1,6 @@
class PasswordsController < Devise::PasswordsController
- include Gitlab::CurrentSettings
-
before_action :resource_from_email, only: [:create]
- before_action :check_password_authentication_available, only: [:create]
+ before_action :prevent_ldap_reset, only: [:create]
before_action :throttle_reset, only: [:create]
def edit
@@ -40,11 +38,11 @@ class PasswordsController < Devise::PasswordsController
self.resource = resource_class.find_by_email(email)
end
- def check_password_authentication_available
- return if current_application_settings.password_authentication_enabled? && (resource.nil? || resource.allow_password_authentication?)
+ def prevent_ldap_reset
+ return unless resource&.ldap_user?
redirect_to after_sending_reset_password_instructions_path_for(resource_name),
- alert: "Password authentication is unavailable."
+ alert: "Cannot reset password for LDAP user."
end
def throttle_reset
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index 17b66df43e7..ddb67d1c4d1 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -1,7 +1,7 @@
class Profiles::EmailsController < Profiles::ApplicationController
def index
@primary = current_user.email
- @emails = current_user.emails
+ @emails = current_user.emails.order_id_desc
end
def create
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 88f49da555a..f9f0e8eef83 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -2,7 +2,7 @@ class Profiles::KeysController < Profiles::ApplicationController
skip_before_action :authenticate_user!, only: [:get_keys]
def index
- @keys = current_user.keys
+ @keys = current_user.keys.order_id_desc
@key = Key.new
end
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index c423761ab24..7beb52dd8e8 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -77,7 +77,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
end
def authorize_change_password!
- render_404 unless @user.allow_password_authentication?
+ render_404 if @user.ldap_user?
end
def user_params
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 1e557c47638..cce2a847b53 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:color_scheme_id,
:layout,
:dashboard,
- :project_view
+ :project_view,
+ :theme_id
)
end
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 076076fd1b3..d83824fef06 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -9,8 +9,6 @@ class ProfilesController < Profiles::ApplicationController
end
def update
- user_params.except!(:email) if @user.external_email?
-
respond_to do |format|
result = Users::UpdateService.new(@user, user_params).execute
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 221e01b415a..d7dd8ddcb7d 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -94,6 +94,6 @@ class Projects::ApplicationController < ApplicationController
end
def require_pages_enabled!
- not_found unless Gitlab.config.pages.enabled
+ not_found unless @project.pages_available?
end
end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index f637a9a803b..eb010923466 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -7,7 +7,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :authorize_update_build!, only: [:keep]
before_action :extract_ref_name_and_path
before_action :validate_artifacts!
- before_action :set_path_and_entry, only: [:file, :raw]
+ before_action :entry, only: [:file]
def download
if artifacts_file.file_storage?
@@ -41,7 +41,10 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def raw
- send_artifacts_entry(build, @entry)
+ path = Gitlab::Ci::Build::Artifacts::Path
+ .new(params[:path])
+
+ send_artifacts_entry(build, path)
end
def keep
@@ -93,9 +96,8 @@ class Projects::ArtifactsController < Projects::ApplicationController
@artifacts_file ||= build.artifacts_file
end
- def set_path_and_entry
- @path = params[:path]
- @entry = build.artifacts_metadata_entry(@path)
+ def entry
+ @entry = build.artifacts_metadata_entry(params[:path])
render_404 unless @entry.exists?
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index a2e8c10857d..2b8f3977e6e 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -198,6 +198,10 @@ class Projects::BlobController < Projects::ApplicationController
json = blob_json(@blob)
return render_404 unless json
+ path_segments = @path.split('/')
+ path_segments.pop
+ tree_path = path_segments.join('/')
+
render json: json.merge(
path: blob.path,
name: blob.name,
@@ -212,6 +216,7 @@ class Projects::BlobController < Projects::ApplicationController
raw_path: project_raw_path(project, @id),
blame_path: project_blame_path(project, @id),
commits_path: project_commits_path(project, @id),
+ tree_path: project_tree_path(project, File.join(@ref, tree_path)),
permalink: project_blob_path(project, File.join(@commit.id, @path))
)
end
diff --git a/app/controllers/projects/boards/application_controller.rb b/app/controllers/projects/boards/application_controller.rb
deleted file mode 100644
index dad38fff6b9..00000000000
--- a/app/controllers/projects/boards/application_controller.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Projects
- module Boards
- class ApplicationController < Projects::ApplicationController
- respond_to :json
-
- rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
-
- private
-
- def record_not_found(exception)
- render json: { error: exception.message }, status: :not_found
- end
- end
- end
-end
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
deleted file mode 100644
index 653e7bc7e40..00000000000
--- a/app/controllers/projects/boards/issues_controller.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-module Projects
- module Boards
- class IssuesController < Boards::ApplicationController
- before_action :authorize_read_issue!, only: [:index]
- before_action :authorize_create_issue!, only: [:create]
- before_action :authorize_update_issue!, only: [:update]
-
- def index
- issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
- issues = issues.page(params[:page]).per(params[:per] || 20)
- make_sure_position_is_set(issues)
-
- render json: {
- issues: serialize_as_json(issues),
- size: issues.total_count
- }
- end
-
- def create
- service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
- issue = service.execute
-
- if issue.valid?
- render json: serialize_as_json(issue)
- else
- render json: issue.errors, status: :unprocessable_entity
- end
- end
-
- def update
- service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
-
- if service.execute(issue)
- head :ok
- else
- head :unprocessable_entity
- end
- end
-
- private
-
- def make_sure_position_is_set(issues)
- issues.each do |issue|
- issue.move_to_end && issue.save unless issue.relative_position
- end
- end
-
- def issue
- @issue ||=
- IssuesFinder.new(current_user, project_id: project.id)
- .execute
- .where(iid: params[:id])
- .first!
- end
-
- def authorize_read_issue!
- return render_403 unless can?(current_user, :read_issue, project)
- end
-
- def authorize_create_issue!
- return render_403 unless can?(current_user, :admin_issue, project)
- end
-
- def authorize_update_issue!
- return render_403 unless can?(current_user, :update_issue, issue)
- end
-
- def filter_params
- params.merge(board_id: params[:board_id], id: params[:list_id])
- .reject { |_, value| value.nil? }
- end
-
- def move_params
- params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_iid, :move_after_iid)
- end
-
- def issue_params
- params.require(:issue).permit(:title).merge(board_id: params[:board_id], list_id: params[:list_id], request: request)
- end
-
- def serialize_as_json(resource)
- resource.as_json(
- labels: true,
- only: [:id, :iid, :title, :confidential, :due_date, :relative_position],
- include: {
- assignees: { only: [:id, :name, :username], methods: [:avatar_url] },
- milestone: { only: [:id, :title] }
- },
- user: current_user
- )
- end
- end
- end
-end
diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb
deleted file mode 100644
index ad53bb749a0..00000000000
--- a/app/controllers/projects/boards/lists_controller.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-module Projects
- module Boards
- class ListsController < Boards::ApplicationController
- before_action :authorize_admin_list!, only: [:create, :update, :destroy, :generate]
- before_action :authorize_read_list!, only: [:index]
-
- def index
- lists = ::Boards::Lists::ListService.new(project, current_user).execute(board)
-
- render json: serialize_as_json(lists)
- end
-
- def create
- list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute(board)
-
- if list.valid?
- render json: serialize_as_json(list)
- else
- render json: list.errors, status: :unprocessable_entity
- end
- end
-
- def update
- list = board.lists.movable.find(params[:id])
- service = ::Boards::Lists::MoveService.new(project, current_user, move_params)
-
- if service.execute(list)
- head :ok
- else
- head :unprocessable_entity
- end
- end
-
- def destroy
- list = board.lists.destroyable.find(params[:id])
- service = ::Boards::Lists::DestroyService.new(project, current_user)
-
- if service.execute(list)
- head :ok
- else
- head :unprocessable_entity
- end
- end
-
- def generate
- service = ::Boards::Lists::GenerateService.new(project, current_user)
-
- if service.execute(board)
- render json: serialize_as_json(board.lists.movable)
- else
- head :unprocessable_entity
- end
- end
-
- private
-
- def authorize_admin_list!
- return render_403 unless can?(current_user, :admin_list, project)
- end
-
- def authorize_read_list!
- return render_403 unless can?(current_user, :read_list, project)
- end
-
- def board
- @board ||= project.boards.find(params[:board_id])
- end
-
- def list_params
- params.require(:list).permit(:label_id)
- end
-
- def move_params
- params.require(:list).permit(:position)
- end
-
- def serialize_as_json(resource)
- resource.as_json(
- only: [:id, :list_type, :position],
- methods: [:title],
- label: true
- )
- end
- end
- end
-end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 808affa4f98..d1b99ecce4a 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -1,32 +1,31 @@
class Projects::BoardsController < Projects::ApplicationController
+ include BoardsResponses
include IssuableCollections
before_action :authorize_read_board!, only: [:index, :show]
+ before_action :assign_endpoint_vars
def index
- @boards = ::Boards::ListService.new(project, current_user).execute
-
- respond_to do |format|
- format.html
- format.json do
- render json: serialize_as_json(@boards)
- end
- end
+ @boards = Boards::ListService.new(project, current_user).execute
+
+ respond_with_boards
end
def show
@board = project.boards.find(params[:id])
- respond_to do |format|
- format.html
- format.json do
- render json: serialize_as_json(@board)
- end
- end
+ respond_with_board
end
private
+ def assign_endpoint_vars
+ @boards_endpoint = project_boards_url(project)
+ @bulk_issues_path = bulk_update_project_issues_path(project)
+ @namespace_path = project.namespace.full_path
+ @labels_endpoint = project_labels_path(project)
+ end
+
def authorize_read_board!
return access_denied! unless can?(current_user, :read_board, project)
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 6de125e7e80..1a775def506 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -127,7 +127,7 @@ class Projects::CommitController < Projects::ApplicationController
@discussions = commit.discussions
@notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes)
- @notes = prepare_notes_for_rendering(@notes)
+ @notes = prepare_notes_for_rendering(@notes, @commit)
end
def assign_change_commit_vars
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 2de9900d449..4a841bf2073 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -2,6 +2,7 @@ require "base64"
class Projects::CommitsController < Projects::ApplicationController
include ExtractsPath
+ include RendersCommits
before_action :require_non_empty_project
before_action :assign_ref_vars
@@ -56,5 +57,7 @@ class Projects::CommitsController < Projects::ApplicationController
else
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end
+
+ @commits = prepare_commits_for_rendering(@commits)
end
end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index c8613c0d634..193549663ac 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -3,6 +3,7 @@ require 'addressable/uri'
class Projects::CompareController < Projects::ApplicationController
include DiffForPath
include DiffHelper
+ include RendersCommits
# Authorize
before_action :require_non_empty_project
@@ -50,7 +51,7 @@ class Projects::CompareController < Projects::ApplicationController
.execute(@project, @start_ref)
if @compare
- @commits = @compare.commits
+ @commits = prepare_commits_for_rendering(@compare.commits)
@diffs = @compare.diffs(diff_options)
environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @compare.commit }
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
index b69d46f2c41..26f3c114108 100644
--- a/app/controllers/projects/cycle_analytics/events_controller.rb
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -2,7 +2,7 @@ module Projects
module CycleAnalytics
class EventsController < Projects::ApplicationController
include CycleAnalyticsParams
-
+
before_action :authorize_read_cycle_analytics!
before_action :authorize_read_build!, only: [:test, :staging]
before_action :authorize_read_issue!, only: [:issue, :production]
@@ -11,33 +11,33 @@ module Projects
def issue
render_events(cycle_analytics[:issue].events)
end
-
+
def plan
render_events(cycle_analytics[:plan].events)
end
-
+
def code
render_events(cycle_analytics[:code].events)
end
-
+
def test
options(events_params)[:branch] = events_params[:branch_name]
-
+
render_events(cycle_analytics[:test].events)
end
-
+
def review
render_events(cycle_analytics[:review].events)
end
-
+
def staging
render_events(cycle_analytics[:staging].events)
end
-
+
def production
render_events(cycle_analytics[:production].events)
end
-
+
private
def render_events(events)
@@ -46,14 +46,14 @@ module Projects
format.json { render json: { events: events } }
end
end
-
+
def cycle_analytics
@cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params))
end
-
+
def events_params
return {} unless params[:events].present?
-
+
params[:events].permit(:start_date, :branch_name)
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index e2ccabb22db..ab9f132b502 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -15,7 +15,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue
- before_action :authorize_update_issue!, only: [:edit, :update]
+ before_action :authorize_update_issue!, only: [:edit, :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]
@@ -27,10 +27,9 @@ class Projects::IssuesController < Projects::ApplicationController
@issues = issues_collection
@issues = @issues.page(params[:page])
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
+ @total_pages = issues_page_count(@issues)
- if @issues.out_of_range? && @issues.total_pages != 0
- return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true))
- end
+ 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
@@ -86,16 +85,30 @@ class Projects::IssuesController < Projects::ApplicationController
@note = @project.notes.new(noteable: @issue)
@discussions = @issue.discussions
- @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
+ @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
respond_to do |format|
format.html
format.json do
- render json: IssueSerializer.new.represent(@issue)
+ render json: serializer.represent(@issue)
end
end
end
+ def discussions
+ notes = @issue.notes
+ .inc_relations_for_view
+ .includes(:noteable)
+ .fresh
+ .reject { |n| n.cross_reference_not_visible_for?(current_user) }
+
+ prepare_notes_for_rendering(notes)
+
+ discussions = Discussion.build_collection(notes, @issue)
+
+ render json: DiscussionSerializer.new(project: @project, noteable: @issue, current_user: current_user).represent(discussions)
+ end
+
def create
create_params = issue_params.merge(spammable_params).merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
@@ -128,25 +141,33 @@ 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
+ end
+
+ rescue ActiveRecord::StaleObjectError
+ render_conflict_response
+ end
+
+ def move
+ params.require(:move_to_project_id)
+
if params[:move_to_project_id].to_i > 0
new_project = Project.find(params[:move_to_project_id])
return render_404 unless issue.can_move?(current_user, new_project)
- move_service = Issues::MoveService.new(project, current_user)
- @issue = move_service.execute(@issue, new_project)
+ @issue = Issues::UpdateService.new(project, current_user, target_project: new_project).execute(issue)
end
respond_to do |format|
- format.html do
- recaptcha_check_with_fallback { render :edit }
- end
-
format.json do
- if @issue.valid?
- render json: IssueSerializer.new.represent(@issue)
- else
- render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
- end
+ render_issue_json
end
end
@@ -202,7 +223,7 @@ class Projects::IssuesController < Projects::ApplicationController
task_status: @issue.task_status
}
- if @issue.is_edited?
+ if @issue.edited?
response[:updated_at] = @issue.updated_at
response[:updated_by_name] = @issue.last_edited_by.name
response[:updated_by_path] = user_path(@issue.last_edited_by)
@@ -212,7 +233,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create_merge_request
- result = MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
+ result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
@@ -257,15 +278,11 @@ class Projects::IssuesController < Projects::ApplicationController
return render_404 unless @project.feature_available?(:issues, current_user)
end
- def redirect_to_external_issue_tracker
- external = @project.external_issue_tracker
-
- return unless external
-
- if action_name == 'new'
- redirect_to external.new_issue_path
+ def render_issue_json
+ if @issue.valid?
+ render json: serializer.represent(@issue)
else
- redirect_to external.issue_tracker_path
+ render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
end
end
@@ -299,4 +316,8 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_to new_user_session_path, notice: notice
end
+
+ def serializer
+ IssueSerializer.new(current_user: current_user, project: issue.project)
+ end
end
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index f35d53896ba..1096afbb798 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -1,6 +1,7 @@
class Projects::MergeRequests::CreationsController < Projects::MergeRequests::ApplicationController
include DiffForPath
include DiffHelper
+ include RendersCommits
skip_before_action :merge_request
skip_before_action :ensure_ref_fetched
@@ -107,7 +108,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@target_project = @merge_request.target_project
@source_project = @merge_request.source_project
- @commits = @merge_request.commits
+ @commits = prepare_commits_for_rendering(@merge_request.commits)
@commit = @merge_request.diff_head_commit
@note_counts = Note.where(commit_id: @commits.map(&:id))
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 330b7df4541..d60a24d3f1d 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -27,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@merge_request.merge_request_diff
end
- @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
+ @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff.order_id_desc
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
@@ -61,6 +61,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
@grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs)
- @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes))
+ @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request)
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 4de814d0ca8..3aa5dadb5ca 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -2,6 +2,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include ToggleSubscriptionAction
include IssuableActions
include RendersNotes
+ include RendersCommits
include ToggleAwardEmoji
include IssuableCollections
@@ -18,10 +19,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(merge_request_diff: :merge_request)
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
+ @total_pages = merge_requests_page_count(@merge_requests)
- if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
- return redirect_to url_for(params.merge(page: @merge_requests.total_pages, only_path: true))
- end
+ return if redirect_out_of_range(@merge_requests, @total_pages)
if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] }
@@ -61,12 +61,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
# Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request)
- @discussions = @merge_request.discussions
- @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
-
@noteable = @merge_request
@commits_count = @merge_request.commits_count
+ @discussions = @merge_request.discussions
+ @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
+
labels
set_pipeline_variables
@@ -95,7 +95,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def commits
# Get commits from repository
# or from cache if already merged
- @commits = @merge_request.commits
+ @commits = prepare_commits_for_rendering(@merge_request.commits)
@note_counts = Note.where(commit_id: @commits.map(&:id))
.group(:commit_id).count
@@ -218,8 +218,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
if can?(current_user, :read_environment, environment) && environment.has_metrics?
metrics_project_environment_deployment_path(environment.project, environment, deployment)
end
-
- metrics_monitoring_url =
+
+ metrics_monitoring_url =
if can?(current_user, :read_environment, environment)
environment_metrics_path(environment)
end
@@ -318,14 +318,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
elsif @merge_request.head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
- MergeWorker.perform_async(@merge_request.id, current_user.id, params)
+ @merge_request.merge_async(current_user.id, params)
:success
else
:failed
end
else
- MergeWorker.perform_async(@merge_request.id, current_user.id, params)
+ @merge_request.merge_async(current_user.id, params)
:success
end
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 9d24ebe2138..abab2e2f0c9 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -6,7 +6,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
end
def update
- if @project.update_attributes(update_params)
+ if @project.update(update_params)
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
redirect_to project_settings_ci_cd_path(@project)
else
@@ -16,14 +16,12 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
private
- def create_params
- params.require(:pipeline).permit(:ref)
- end
-
def update_params
params.require(:project).permit(
- :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
- :public_builds, :auto_cancel_pending_pipelines, :ci_config_path
+ :runners_token, :builds_enabled, :build_allow_git_fetch,
+ :build_timeout_in_minutes, :build_coverage_regex, :public_builds,
+ :auto_cancel_pending_pipelines, :ci_config_path,
+ auto_devops_attributes: [:id, :domain, :enabled]
)
end
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index f8ff7413b53..d925dcd21ff 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -47,6 +47,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
end
+ def import
+ @projects = current_user.authorized_projects.order_id_desc
+ end
+
def apply_import
source_project = Project.find(params[:source_project_id])
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index d54a1111f11..daa5c88aae0 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -4,7 +4,6 @@ class Projects::ServicesController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
before_action :service, only: [:edit, :update, :test]
- before_action :update_service, only: [:update, :test]
respond_to :html
@@ -14,6 +13,8 @@ class Projects::ServicesController < Projects::ApplicationController
end
def update
+ @service.attributes = service_params[:service]
+
if @service.save(context: :manual_change)
redirect_to(project_settings_integrations_path(@project), notice: success_message)
else
@@ -24,7 +25,7 @@ class Projects::ServicesController < Projects::ApplicationController
def test
message = {}
- if @service.can_test?
+ if @service.can_test? && @service.update_attributes(service_params[:service])
data = @service.test_data(project, current_user)
outcome = @service.test(data)
@@ -50,10 +51,6 @@ class Projects::ServicesController < Projects::ApplicationController
end
end
- def update_service
- @service.assign_attributes(service_params[:service])
- end
-
def service
@service ||= @project.find_or_initialize_service(params[:id])
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 15a2ff56b92..b029b31f9af 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -8,6 +8,7 @@ module Projects
define_secret_variables
define_triggers_variables
define_badges_variables
+ define_auto_devops_variables
end
private
@@ -42,6 +43,10 @@ module Projects
badge.new(@project, @ref).metadata
end
end
+
+ def define_auto_devops_variables
+ @auto_devops = @project.auto_devops || ProjectAutoDevops.new
+ end
end
end
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index d07143d294f..7c19aa7bb23 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -64,7 +64,7 @@ class Projects::SnippetsController < Projects::ApplicationController
@noteable = @snippet
@discussions = @snippet.discussions
- @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
+ @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
render 'show'
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 8dfe0f51709..b13034d3333 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -7,6 +7,7 @@ class ProjectsController < Projects::ApplicationController
before_action :repository, except: [:index, :new, :create]
before_action :assign_ref_vars, only: [:show], if: :repo_exists?
before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?]
+ before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
# Authorize
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
@@ -19,7 +20,10 @@ class ProjectsController < Projects::ApplicationController
end
def new
- @project = Project.new
+ namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id]
+ return access_denied! if namespace && !can?(current_user, :create_projects, namespace)
+
+ @project = Project.new(namespace_id: namespace&.id)
end
def edit
@@ -301,10 +305,11 @@ class ProjectsController < Projects::ApplicationController
end
def load_events
- @events = @project.events.recent
- @events = event_filter.apply_filter(@events).with_associations
- limit = (params[:limit] || 20).to_i
- @events = @events.limit(limit).offset(params[:offset] || 0)
+ projects = Project.where(id: @project.id)
+
+ @events = EventCollection
+ .new(projects, offset: params[:offset].to_i, filter: event_filter)
+ .to_a
end
def project_params
@@ -318,6 +323,7 @@ class ProjectsController < Projects::ApplicationController
:build_allow_git_fetch,
:build_coverage_regex,
:build_timeout_in_minutes,
+ :resolve_outdated_diff_discussions,
:container_registry_enabled,
:default_branch,
:description,
@@ -389,4 +395,8 @@ class ProjectsController < Projects::ApplicationController
url_for(params)
end
+
+ def project_export_enabled
+ render_404 unless current_application_settings.project_export_enabled?
+ end
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index d58c8d14a75..fbad9ba7db8 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -2,6 +2,7 @@ class SearchController < ApplicationController
skip_before_action :authenticate_user!
include SearchHelper
+ include RendersCommits
layout 'search'
@@ -20,6 +21,8 @@ class SearchController < ApplicationController
@search_results = search_service.search_results
@search_objects = search_service.search_objects
+ render_commits if @scope == 'commits'
+
check_single_commit_result
end
@@ -38,6 +41,10 @@ class SearchController < ApplicationController
private
+ def render_commits
+ @search_objects = prepare_commits_for_rendering(@search_objects)
+ end
+
def check_single_commit_result
if @search_results.single_commit_result?
only_commit = @search_results.objects('commits').first
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 9e743685d60..be6491d042c 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -5,14 +5,6 @@ class SessionsController < Devise::SessionsController
skip_before_action :check_two_factor_requirement, only: [:destroy]
- # Explicitly call protect from forgery before anything else. Otherwise the
- # CSFR-token might be cleared before authentication is done. This was the case
- # when LDAP was enabled and the `OmniauthCallbacksController` is loaded
- #
- # *Note:* `prepend: true` is the default for rails4, but this will be changed
- # to `prepend: false` in rails5.
- protect_from_forgery prepend: true, with: :exception
-
prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create]
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 8c3abd0a085..c1cdc7c9831 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -66,7 +66,7 @@ class SnippetsController < ApplicationController
@noteable = @snippet
@discussions = @snippet.discussions
- @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
+ @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
respond_to do |format|
format.html do
diff --git a/app/controllers/unicorn_test_controller.rb b/app/controllers/unicorn_test_controller.rb
index b7a1a046be0..ed04bd1f77d 100644
--- a/app/controllers/unicorn_test_controller.rb
+++ b/app/controllers/unicorn_test_controller.rb
@@ -1,12 +1,14 @@
+# :nocov:
if Rails.env.test?
class UnicornTestController < ActionController::Base
def pid
render plain: Process.pid.to_s
end
-
+
def kill
Process.kill(params[:signal], Process.pid)
render plain: 'Bye!'
end
end
end
+# :nocov:
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index dc882b17143..16a74f82d3f 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -89,7 +89,7 @@ class UploadsController < ApplicationController
@uploader.retrieve_from_store!(params[:filename])
else
- @uploader = @model.send(upload_mount)
+ @uploader = @model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend
redirect_to @uploader.url unless @uploader.file_storage?
end
diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb
index a5ba791a513..d6bcd939522 100644
--- a/app/finders/admin/projects_finder.rb
+++ b/app/finders/admin/projects_finder.rb
@@ -1,33 +1,67 @@
class Admin::ProjectsFinder
- attr_reader :sort, :namespace_id, :visibility_level, :with_push,
- :abandoned, :last_repository_check_failed, :archived,
- :personal, :name, :page, :current_user
+ attr_reader :params, :current_user
def initialize(params:, current_user:)
+ @params = params
@current_user = current_user
- @sort = params.fetch(:sort) { 'latest_activity_desc' }
- @namespace_id = params[:namespace_id]
- @visibility_level = params[:visibility_level]
- @with_push = params[:with_push]
- @abandoned = params[:abandoned]
- @last_repository_check_failed = params[:last_repository_check_failed]
- @archived = params[:archived]
- @personal = params[:personal]
- @name = params[:name]
- @page = params[:page]
end
def execute
- items = Project.with_statistics
- items = items.in_namespace(namespace_id) if namespace_id.present?
- items = items.where(visibility_level: visibility_level) if visibility_level.present?
- items = items.with_push if with_push.present?
- items = items.abandoned if abandoned.present?
- items = items.where(last_repository_check_failed: true) if last_repository_check_failed.present?
- items = items.non_archived unless archived.present?
- items = items.personal(current_user) if personal.present?
- items = items.search(name) if name.present?
- items = items.sort(sort)
- items.includes(:namespace).order("namespaces.path, projects.name ASC").page(page)
+ items = Project.without_deleted.with_statistics
+ items = by_namespace_id(items)
+ items = by_visibilty_level(items)
+ items = by_with_push(items)
+ items = by_abandoned(items)
+ items = by_last_repository_check_failed(items)
+ items = by_archived(items)
+ items = by_personal(items)
+ items = by_name(items)
+ items = sort(items)
+ items.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
+ end
+
+ private
+
+ def by_namespace_id(items)
+ params[:namespace_id].present? ? items.in_namespace(params[:namespace_id]) : items
+ end
+
+ def by_visibilty_level(items)
+ params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
+ end
+
+ def by_with_push(items)
+ params[:with_push].present? ? items.with_push : items
+ end
+
+ def by_abandoned(items)
+ params[:abandoned].present? ? items.abandoned : items
+ end
+
+ def by_last_repository_check_failed(items)
+ params[:last_repository_check_failed].present? ? items.where(last_repository_check_failed: true) : items
+ end
+
+ def by_archived(items)
+ if params[:archived] == 'only'
+ items.archived
+ elsif params[:archived].blank?
+ items.non_archived
+ else
+ items
+ end
+ end
+
+ def by_personal(items)
+ params[:personal].present? ? items.personal(current_user) : items
+ end
+
+ def by_name(items)
+ params[:name].present? ? items.search(params[:name]) : items
+ end
+
+ def sort(items)
+ sort = params.fetch(:sort) { 'latest_activity_desc' }
+ items.sort(sort)
end
end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index e6fb112e7f2..88d71b0a87b 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -1,3 +1,19 @@
+# GroupsFinder
+#
+# Used to filter Groups by a set of params
+#
+# Arguments:
+# current_user - which user is requesting groups
+# params:
+# owned: boolean
+# parent: Group
+# all_available: boolean (defaults to true)
+#
+# Users with full private access can see all groups. The `owned` and `parent`
+# params can be used to restrict the groups that are returned.
+#
+# Anonymous users will never return any `owned` groups. They will return all
+# public groups instead, even if `all_available` is set to false.
class GroupsFinder < UnionFinder
def initialize(current_user = nil, params = {})
@current_user = current_user
@@ -16,13 +32,13 @@ class GroupsFinder < UnionFinder
attr_reader :current_user, :params
def all_groups
- groups = []
-
- if current_user
- groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups
- end
- groups << Group.unscoped.public_to_user(current_user)
+ return [owned_groups] if params[:owned]
+ return [Group.all] if current_user&.full_private_access?
+ groups = []
+ groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups if current_user
+ groups << Group.unscoped.public_to_user(current_user) if include_public_groups?
+ groups << Group.none if groups.empty?
groups
end
@@ -39,4 +55,12 @@ class GroupsFinder < UnionFinder
groups.where(parent: params[:parent])
end
+
+ def owned_groups
+ current_user&.groups || Group.none
+ end
+
+ def include_public_groups?
+ current_user.nil? || params.fetch(:all_available, true)
+ end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 08a843ada97..9848497f258 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -18,12 +18,12 @@
# sort: string
# non_archived: boolean
# iids: integer[]
+# my_reaction_emoji: string
#
class IssuableFinder
include CreatedAtFilter
NONE = '0'.freeze
- IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page state].freeze
attr_accessor :current_user, :params
@@ -46,6 +46,7 @@ class IssuableFinder
items = by_iids(items)
items = by_milestone(items)
items = by_label(items)
+ items = by_my_reaction_emoji(items)
# Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far
items = by_project(items)
@@ -60,13 +61,17 @@ class IssuableFinder
execute.find_by(*params)
end
+ def row_count
+ Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state])
+ end
+
# We often get counts for each state by running a query per state, and
# counting those results. This is typically slower than running one query
# (even if that query is slower than any of the individual state queries) and
# grouping and counting within that query.
#
def count_by_state
- count_params = params.merge(state: nil, sort: nil, for_counting: true)
+ count_params = params.merge(state: nil, sort: nil)
labels_count = label_names.any? ? label_names.count : 1
finder = self.class.new(current_user, count_params)
counts = Hash.new(0)
@@ -89,16 +94,6 @@ class IssuableFinder
execute.find_by!(*params)
end
- def state_counter_cache_key
- cache_key(state_counter_cache_key_components)
- end
-
- def clear_caches!
- state_counter_cache_key_components_permutations.each do |components|
- Rails.cache.delete(cache_key(components))
- end
- end
-
def group
return @group if defined?(@group)
@@ -371,6 +366,14 @@ class IssuableFinder
items
end
+ def by_my_reaction_emoji(items)
+ if params[:my_reaction_emoji].present? && current_user
+ items = items.awarded(current_user, params[:my_reaction_emoji])
+ end
+
+ items
+ end
+
def by_due_date(items)
if due_date?
if filter_by_no_due_date?
@@ -422,20 +425,4 @@ class IssuableFinder
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
-
- def state_counter_cache_key_components
- opts = params.with_indifferent_access
- opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
- opts.delete_if { |_, value| value.blank? }
-
- ['issuables_count', klass.to_ability_name, opts.sort]
- end
-
- def state_counter_cache_key_components_permutations
- [state_counter_cache_key_components]
- end
-
- def cache_key(components)
- Digest::SHA1.hexdigest(components.flatten.join('-'))
- end
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 0ec42a4e6eb..d2275139c42 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -14,6 +14,7 @@
# search: string
# label_name: string
# sort: string
+# my_reaction_emoji: string
#
class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
@@ -54,44 +55,10 @@ class IssuesFinder < IssuableFinder
project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL
end
- # Anonymous users can't see any confidential issues.
- #
- # Users without access to see _all_ confidential issues (as in
- # `user_can_see_all_confidential_issues?`) are more complicated, because they
- # can see confidential issues where:
- # 1. They are an assignee.
- # 2. They are an author.
- #
- # That's fine for most cases, but if we're just counting, we need to cache
- # effectively. If we cached this accurately, we'd have a cache key for every
- # authenticated user without sufficient access to the project. Instead, when
- # we are counting, we treat them as if they can't see any confidential issues.
- #
- # This does mean the counts may be wrong for those users, but avoids an
- # explosion in cache keys.
- def user_cannot_see_confidential_issues?(for_counting: false)
+ def user_cannot_see_confidential_issues?
return false if user_can_see_all_confidential_issues?
- current_user.blank? || for_counting || params[:for_counting]
- end
-
- def state_counter_cache_key_components
- extra_components = [
- user_can_see_all_confidential_issues?,
- user_cannot_see_confidential_issues?(for_counting: true)
- ]
-
- super + extra_components
- end
-
- def state_counter_cache_key_components_permutations
- # Ignore the last two, as we'll provide both options for them.
- components = super.first[0..-3]
-
- [
- components + [false, true],
- components + [true, false]
- ]
+ current_user.blank?
end
def by_assignee(items)
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 771da3d441d..d0687d28c21 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -16,6 +16,7 @@
# label_name: string
# sort: string
# non_archived: boolean
+# my_reaction_emoji: string
#
class MergeRequestsFinder < IssuableFinder
def klass
diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb
index 79eb45568be..038d5565a1e 100644
--- a/app/finders/move_to_project_finder.rb
+++ b/app/finders/move_to_project_finder.rb
@@ -9,6 +9,7 @@ class MoveToProjectFinder
projects = @user.projects_where_can_admin_issues
projects = projects.search(search) if search.present?
projects = projects.excluding_project(from_project)
+ projects = projects.order_id_desc
# infinite scroll using offset
projects = projects.where('projects.id < ?', offset_id) if offset_id.present?
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index aa80dfc3f37..eac6095d8dc 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -121,13 +121,22 @@ class ProjectsFinder < UnionFinder
end
def sort(items)
- params[:sort].present? ? items.sort(params[:sort]) : items
+ params[:sort].present? ? items.sort(params[:sort]) : items.order_id_desc
end
def by_archived(projects)
- # Back-compatibility with the places where `params[:archived]` can be set explicitly to `false`
- params[:non_archived] = !Gitlab::Utils.to_boolean(params[:archived]) if params.key?(:archived)
-
- params[:non_archived] ? projects.non_archived : projects
+ if params[:non_archived]
+ projects.non_archived
+ elsif params.key?(:archived) # Back-compatibility with the places where `params[:archived]` can be set explicitly to `false`
+ if params[:archived] == 'only'
+ projects.archived
+ elsif Gitlab::Utils.to_boolean(params[:archived])
+ projects
+ else
+ projects.non_archived
+ end
+ else
+ projects
+ end
end
end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index b276116f0c6..3502bf08971 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -118,7 +118,7 @@ class TodosFinder
end
def sort(items)
- params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
+ params[:sort] ? items.sort(params[:sort]) : items.order_id_desc
end
def by_action(items)
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 16136d02530..cdf5fa5d4b7 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -20,7 +20,7 @@ module AppearancesHelper
end
def brand_item
- @appearance ||= Appearance.first
+ @appearance ||= Appearance.current
end
def brand_header_logo
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index bcee81bdc15..8d02d5de5c3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -178,7 +178,7 @@ module ApplicationHelper
end
def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', exclude_author: false)
- return unless object.is_edited?
+ return unless object.edited?
content_tag :small, class: 'edited-text' do
output = content_tag(:span, 'Edited ')
@@ -202,7 +202,7 @@ module ApplicationHelper
end
def support_url
- current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
+ Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
end
def page_filter_path(options = {})
@@ -302,10 +302,6 @@ module ApplicationHelper
end
end
- def show_new_nav?
- cookies["new_nav"] == "true"
- end
-
def collapsed_sidebar?
cookies["sidebar_collapsed"] == "true"
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 6825adcb39f..7bd34df5c95 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -1,5 +1,8 @@
module ApplicationSettingsHelper
extend self
+
+ include Gitlab::CurrentSettings
+
delegate :gravatar_enabled?,
:signup_enabled?,
:password_authentication_enabled?,
@@ -81,6 +84,18 @@ module ApplicationSettingsHelper
end
end
+ def key_restriction_options_for_select(type)
+ bit_size_options = Gitlab::SSHPublicKey.supported_sizes(type).map do |bits|
+ ["Must be at least #{bits} bits", bits]
+ end
+
+ [
+ ['Are allowed', 0],
+ *bit_size_options,
+ ['Are forbidden', ApplicationSetting::FORBIDDEN_KEY_VALUE]
+ ]
+ end
+
def repository_storages_options_for_select
options = Gitlab.config.repositories.storages.map do |name, storage|
["#{name} - #{storage['path']}", name]
@@ -100,6 +115,7 @@ module ApplicationSettingsHelper
:after_sign_up_text,
:akismet_api_key,
:akismet_enabled,
+ :auto_devops_enabled,
:clientside_sentry_dsn,
:clientside_sentry_enabled,
:container_registry_token_expire_delay,
@@ -113,9 +129,13 @@ module ApplicationSettingsHelper
:domain_blacklist_enabled,
:domain_blacklist_raw,
:domain_whitelist_raw,
+ :dsa_key_restriction,
+ :ecdsa_key_restriction,
+ :ed25519_key_restriction,
:email_author_in_body,
:enabled_git_access_protocol,
:gravatar_enabled,
+ :hashed_storage_enabled,
:help_page_hide_commercial_content,
:help_page_support_url,
:help_page_text,
@@ -146,6 +166,7 @@ module ApplicationSettingsHelper
:plantuml_enabled,
:plantuml_url,
:polling_interval_multiplier,
+ :project_export_enabled,
:prometheus_metrics_enabled,
:recaptcha_enabled,
:recaptcha_private_key,
@@ -154,6 +175,7 @@ module ApplicationSettingsHelper
:repository_storages,
:require_two_factor_authentication,
:restricted_visibility_levels,
+ :rsa_key_restriction,
:send_user_confirmation_email,
:sentry_dsn,
:sentry_enabled,
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 9c71d6c7f4c..66dc0b1e6f7 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,4 +1,6 @@
module AuthHelper
+ include Gitlab::CurrentSettings
+
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
new file mode 100644
index 00000000000..4ff38f86b5f
--- /dev/null
+++ b/app/helpers/auto_devops_helper.rb
@@ -0,0 +1,7 @@
+module AutoDevopsHelper
+ def show_auto_devops_callout?(project)
+ show_callout?('auto_devops_settings_dismissed') &&
+ can?(current_user, :admin_pipeline, project) &&
+ project.has_auto_devops_implicitly_disabled?
+ end
+end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 4b51269533c..a4c226a6aad 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -12,11 +12,18 @@ module AvatarsHelper
avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name]
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
- data_attributes = { container: 'body' }
+ has_tooltip = options[:has_tooltip].nil? ? true : options[:has_tooltip]
+ data_attributes = {}
+ css_class = %W[avatar s#{avatar_size}].push(*options[:css_class])
+
+ if has_tooltip
+ css_class.push('has-tooltip')
+ data_attributes = { container: 'body' }
+ end
image_tag(
avatar_url,
- class: %W[avatar has-tooltip s#{avatar_size}].push(*options[:css_class]),
+ class: css_class,
alt: "#{user_name}'s avatar",
title: user_name,
data: data_attributes,
diff --git a/app/helpers/blame_helper.rb b/app/helpers/blame_helper.rb
index d1dc4d94560..089d9e3e387 100644
--- a/app/helpers/blame_helper.rb
+++ b/app/helpers/blame_helper.rb
@@ -11,11 +11,15 @@ module BlameHelper
end
def age_map_class(commit_date, duration)
- commit_date_days_ago = (duration[:now] - commit_date).to_i / 1.day
- # Numbers 0 to 10 come from this calculation, but only commits on the oldest
- # day get number 10 (all other numbers can be multiple days), so the range
- # is normalized to 0-9
- age_group = [(10 * commit_date_days_ago) / duration[:started_days_ago], 9].min
- "blame-commit-age-#{age_group}"
+ if duration[:started_days_ago] == 0
+ "blame-commit-age-0"
+ else
+ commit_date_days_ago = (duration[:now] - commit_date).to_i / 1.day
+ # Numbers 0 to 10 come from this calculation, but only commits on the oldest
+ # day get number 10 (all other numbers can be multiple days), so the range
+ # is normalized to 0-9
+ age_group = [(10 * commit_date_days_ago) / duration[:started_days_ago], 9].min
+ "blame-commit-age-#{age_group}"
+ end
end
end
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 8b33c362a9c..4bd61aa8f86 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -1,15 +1,80 @@
module BoardsHelper
- def board_data
- board = @board || @boards.first
+ def board
+ @board ||= @board || @boards.first
+ end
+ def board_data
{
- endpoint: project_boards_path(@project),
+ boards_endpoint: @boards_endpoint,
+ lists_endpoint: board_lists_url(board),
board_id: board.id,
- disabled: "#{!can?(current_user, :admin_list, @project)}",
- issue_link_base: project_issues_path(@project),
+ disabled: "#{!can?(current_user, :admin_list, current_board_parent)}",
+ issue_link_base: build_issue_link_base,
root_path: root_path,
- bulk_update_path: bulk_update_project_issues_path(@project),
+ bulk_update_path: @bulk_issues_path,
default_avatar: image_path(default_avatar)
}
end
+
+ def build_issue_link_base
+ project_issues_path(@project)
+ end
+
+ def current_board_json
+ board = @board || @boards.first
+
+ board.to_json(
+ only: [:id, :name, :milestone_id],
+ include: {
+ milestone: { only: [:title] }
+ }
+ )
+ end
+
+ def board_base_url
+ project_boards_path(@project)
+ end
+
+ def multiple_boards_available?
+ current_board_parent.multiple_issue_boards_available?(current_user)
+ end
+
+ def current_board_path(board)
+ @current_board_path ||= project_board_path(current_board_parent, board)
+ end
+
+ def current_board_parent
+ @current_board_parent ||= @project
+ end
+
+ def can_admin_issue?
+ can?(current_user, :admin_issue, current_board_parent)
+ end
+
+ def board_list_data
+ {
+ toggle: "dropdown",
+ list_labels_path: labels_filter_path(true),
+ labels: labels_filter_path(true),
+ labels_endpoint: @labels_endpoint,
+ namespace_path: @namespace_path,
+ project_path: @project&.try(:path)
+ }
+ end
+
+ def board_sidebar_user_data
+ dropdown_options = issue_assignees_dropdown_options
+
+ {
+ toggle: 'dropdown',
+ field_name: 'issue[assignee_ids][]',
+ first_user: current_user&.username,
+ current_user: 'true',
+ project_id: @project&.try(:id),
+ null_user: 'true',
+ multi_select: 'true',
+ 'dropdown-header': dropdown_options[:data][:'dropdown-header'],
+ 'max-select': dropdown_options[:data][:'max-select']
+ }
+ end
end
diff --git a/app/helpers/breadcrumbs_helper.rb b/app/helpers/breadcrumbs_helper.rb
index abe8edd6a8c..ee1b7ed083e 100644
--- a/app/helpers/breadcrumbs_helper.rb
+++ b/app/helpers/breadcrumbs_helper.rb
@@ -22,4 +22,16 @@ module BreadcrumbsHelper
@breadcrumb_title = title
end
+
+ def breadcrumb_list_item(link)
+ content_tag "li" do
+ link + icon("angle-right", class: "breadcrumbs-list-angle")
+ end
+ end
+
+ def add_to_breadcrumb_dropdown(link, location: :before)
+ @breadcrumb_dropdown_links ||= {}
+ @breadcrumb_dropdown_links[location] ||= []
+ @breadcrumb_dropdown_links[location] << link
+ end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index bf9ad95b7c2..48cf30a48ab 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -20,6 +20,9 @@ module ButtonHelper
def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent'
title = data[:title] || 'Copy to clipboard'
+ button_text = data[:button_text] || ''
+ hide_tooltip = data[:hide_tooltip] || false
+ hide_button_icon = data[:hide_button_icon] || false
# This supports code in app/assets/javascripts/copy_to_clipboard.js that
# works around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM.
@@ -35,17 +38,22 @@ module ButtonHelper
target = data.delete(:target)
data[:clipboard_target] = target if target
- data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
+ unless hide_tooltip
+ data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
+ end
- content_tag :button,
- icon('clipboard', 'aria-hidden': 'true'),
+ button_attributes = {
class: "btn #{css_class}",
data: data,
type: :button,
title: title,
- aria: {
- label: title
- }
+ aria: { label: title }
+ }
+
+ content_tag :button, button_attributes do
+ concat(icon('clipboard', 'aria-hidden': 'true')) unless hide_button_icon
+ concat(button_text)
+ end
end
def http_clone_button(project, placement = 'right', append_link: true)
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 69220a1c0f6..9651f9733f9 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -114,7 +114,7 @@ module CommitsHelper
end
def commit_signature_badge_classes(additional_classes)
- %w(btn status-box gpg-status-box) + Array(additional_classes)
+ %w(btn gpg-status-box) + Array(additional_classes)
end
protected
@@ -128,10 +128,10 @@ module CommitsHelper
# avatar: true will prepend the avatar image
# size: size of the avatar image in px
def commit_person_link(commit, options = {})
- user = commit.send(options[:source])
+ user = commit.public_send(options[:source]) # rubocop:disable GitlabSecurity/PublicSend
- source_name = clean(commit.send "#{options[:source]}_name".to_sym)
- source_email = clean(commit.send "#{options[:source]}_email".to_sym)
+ source_name = clean(commit.public_send(:"#{options[:source]}_name")) # rubocop:disable GitlabSecurity/PublicSend
+ source_email = clean(commit.public_send(:"#{options[:source]}_email")) # rubocop:disable GitlabSecurity/PublicSend
person_name = user.try(:name) || source_name
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index ff305fa39b4..5089da519df 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -97,9 +97,11 @@ module DropdownsHelper
end
end
- def dropdown_footer(&block)
+ def dropdown_footer(add_content_class: false, &block)
content_tag(:div, class: "dropdown-footer") do
- if block
+ if add_content_class
+ content_tag(:div, capture(&block), class: "dropdown-footer-content")
+ else
capture(&block)
end
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 48c87dca217..b331693c789 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -35,18 +35,18 @@ module EventsHelper
[event.action_name, target].join(" ")
end
- def event_filter_link(key, tooltip)
+ def event_filter_link(key, text, tooltip)
key = key.to_s
active = 'active' if @event_filter.active?(key)
link_opts = {
- class: "event-filter-link",
+ class: "event-filter-link has-tooltip",
id: "#{key}_event_filter",
- title: "Filter by #{tooltip.downcase}"
+ title: tooltip
}
content_tag :li, class: active do
link_to request.path, link_opts do
- content_tag(:span, ' ' + tooltip)
+ content_tag(:span, ' ' + text)
end
end
end
@@ -176,11 +176,12 @@ module EventsHelper
sanitize(
text,
tags: %w(a img gl-emoji b pre code p span),
- attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-name', 'data-unicode-version']
+ attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version']
)
end
def event_commit_title(message)
+ message ||= ''
(message.split("\n").first || "").truncate(70)
rescue
"--broken encoding"
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 9247b1f72de..b5dece38de1 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -1,9 +1,9 @@
module FormHelper
- def form_errors(model)
+ def form_errors(model, type: 'form')
return unless model.errors.any?
pluralized = 'error'.pluralize(model.errors.count)
- headline = "The form contains the following #{pluralized}:"
+ headline = "The #{type} contains the following #{pluralized}:"
content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do
content_tag(:h4, headline) <<
diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb
index 2e9b72e9613..c53ea4519da 100644
--- a/app/helpers/graph_helper.rb
+++ b/app/helpers/graph_helper.rb
@@ -3,7 +3,7 @@ module GraphHelper
refs = ""
# Commit::ref_names already strips the refs/XXX from important refs (e.g. refs/heads/XXX)
# so anything leftover is internally used by GitLab
- commit_refs = commit.ref_names(repo).reject{ |name| name.starts_with?('refs/') }
+ commit_refs = commit.ref_names(repo).reject { |name| name.starts_with?('refs/') }
refs << commit_refs.join(' ')
# append note count
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 8cd61f738e1..36b79da1bde 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -3,6 +3,10 @@ module GroupsHelper
can?(current_user, :change_visibility_level, group)
end
+ def can_change_share_with_group_lock?(group)
+ can?(current_user, :change_share_with_group_lock, group)
+ end
+
def group_icon(group)
if group.is_a?(String)
group = Group.find_by_full_path(group)
@@ -15,18 +19,20 @@ module GroupsHelper
@has_group_title = true
full_title = ''
- group.ancestors.reverse.each do |parent|
- full_title += group_title_link(parent, hidable: true)
-
- full_title += '<span class="hidable"> / </span>'.html_safe
+ 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)
+ else
+ full_title += breadcrumb_list_item group_title_link(parent, hidable: false)
+ end
end
- full_title += group_title_link(group)
- full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name
+ full_title += render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups")
- content_tag :span, class: 'group-title' do
- full_title.html_safe
- end
+ full_title += breadcrumb_list_item group_title_link(group)
+ full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path breadcrumb-item-text js-breadcrumb-item-text') if name
+
+ full_title.html_safe
end
def projects_lfs_status(group)
@@ -59,17 +65,31 @@ module GroupsHelper
end
def remove_group_message(group)
- _("You are going to remove %{group_name}.\nRemoved groups CANNOT be restored!\nAre you ABSOLUTELY sure?") %
+ _("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name }
end
+ def share_with_group_lock_help_text(group)
+ return default_help unless group.parent&.share_with_group_lock?
+
+ if group.share_with_group_lock?
+ if can?(current_user, :change_share_with_group_lock, group.parent)
+ ancestor_locked_but_you_can_override(group)
+ else
+ ancestor_locked_so_ask_the_owner(group)
+ end
+ else
+ ancestor_locked_and_has_been_overridden(group)
+ end
+ end
+
private
- def group_title_link(group, hidable: false)
- link_to(group_path(group), class: "group-path #{'hidable' if hidable}") do
+ 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
output =
- if show_new_nav?
- image_tag(group_icon(group), class: "avatar-tile", width: 16, height: 16)
+ if (group.try(:avatar_url) || show_avatar) && !Rails.env.test?
+ image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15)
else
""
end
@@ -78,4 +98,45 @@ module GroupsHelper
output.html_safe
end
end
+
+ def ancestor_group(group)
+ ancestor = oldest_consecutively_locked_ancestor(group)
+ if can?(current_user, :read_group, ancestor)
+ link_to ancestor.name, group_path(ancestor)
+ else
+ ancestor.name
+ end
+ end
+
+ def remove_the_share_with_group_lock_from_ancestor(group)
+ ancestor = oldest_consecutively_locked_ancestor(group)
+ text = s_("GroupSettings|remove the share with group lock from %{ancestor_group_name}") % { ancestor_group_name: ancestor.name }
+ if can?(current_user, :admin_group, ancestor)
+ link_to text, edit_group_path(ancestor)
+ else
+ text
+ end
+ end
+
+ def oldest_consecutively_locked_ancestor(group)
+ group.ancestors.find do |group|
+ !group.has_parent? || !group.parent.share_with_group_lock?
+ end
+ end
+
+ def default_help
+ s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner.")
+ end
+
+ def ancestor_locked_but_you_can_override(group)
+ s_("GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) }
+ end
+
+ def ancestor_locked_so_ask_the_owner(group)
+ s_("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}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) }
+ end
+
+ def ancestor_locked_and_has_been_overridden(group)
+ s_("GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup.").html_safe % { ancestor_group: ancestor_group(group) }
+ end
end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index a57b5a8fea5..a18ebfb6030 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -5,7 +5,7 @@ module ImportHelper
end
def provider_project_link(provider, path_with_namespace)
- url = __send__("#{provider}_project_url", path_with_namespace)
+ url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend
link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer'
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index f4fad7150e8..66e1e607e01 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -35,7 +35,7 @@ module IssuablesHelper
def serialize_issuable(issuable)
case issuable
when Issue
- IssueSerializer.new.represent(issuable).to_json
+ IssueSerializer.new(current_user: current_user, project: issuable.project).represent(issuable).to_json
when MergeRequest
MergeRequestSerializer
.new(current_user: current_user, project: issuable.project)
@@ -126,22 +126,20 @@ module IssuablesHelper
end
def issuable_meta(issuable, project, text)
- output = content_tag(:strong, class: "identifier") do
- concat("#{text} ")
- concat(to_url_reference(issuable))
- end
-
- output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
+ output = ""
+ output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true)
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
end
output << "&ensp;".html_safe
+ output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!'))
+
output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "hidden-xs hidden-sm")
output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "hidden-md hidden-lg")
- output
+ output.html_safe
end
def issuable_todo(issuable)
@@ -151,7 +149,7 @@ module IssuablesHelper
end
def issuable_labels_tooltip(labels, limit: 5)
- first, last = labels.partition.with_index{ |_, i| i < limit }
+ first, last = labels.partition.with_index { |_, i| i < limit }
label_names = first.collect(&:name)
label_names << "and #{last.size} more" unless last.empty?
@@ -173,8 +171,22 @@ module IssuablesHelper
html.html_safe
end
+ def issuable_first_contribution_icon
+ content_tag(:span, class: 'fa-stack') do
+ concat(icon('certificate', class: "fa-stack-2x"))
+ concat(content_tag(:strong, '1', class: 'fa-inverse fa-stack-1x'))
+ end
+ end
+
def assigned_issuables_count(issuable_type)
- current_user.public_send("assigned_open_#{issuable_type}_count")
+ case issuable_type
+ when :issues
+ current_user.assigned_open_issues_count
+ when :merge_requests
+ current_user.assigned_open_merge_requests_count
+ else
+ raise ArgumentError, "invalid issuable `#{issuable_type}`"
+ end
end
def issuable_filter_params
@@ -200,12 +212,10 @@ module IssuablesHelper
endpoint: project_issue_path(@project, issuable),
canUpdate: can?(current_user, :update_issue, issuable),
canDestroy: can?(current_user, :destroy_issue, issuable),
- canMove: current_user ? issuable.can_move?(current_user) : false,
issuableRef: issuable.to_reference,
isConfidential: issuable.confidential,
- markdownPreviewUrl: preview_markdown_path(@project),
- markdownDocs: help_page_path('user/markdown'),
- projectsAutocompleteUrl: autocomplete_projects_path(project_id: @project.id),
+ markdownPreviewPath: preview_markdown_path(@project),
+ markdownDocsPath: help_page_path('user/markdown'),
issuableTemplates: issuable_templates(issuable),
projectPath: ref_project.path,
projectNamespace: ref_project.namespace.full_path,
@@ -222,7 +232,7 @@ module IssuablesHelper
end
def updated_at_by(issuable)
- return {} unless issuable.is_edited?
+ return {} unless issuable.edited?
{
updatedAt: issuable.updated_at.to_time.iso8601,
@@ -233,16 +243,10 @@ module IssuablesHelper
}
end
- def issuables_count_for_state(issuable_type, state, finder: nil)
- finder ||= public_send("#{issuable_type}_finder")
- cache_key = finder.state_counter_cache_key
-
- @counts ||= {}
- @counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
- finder.count_by_state
- end
+ def issuables_count_for_state(issuable_type, state)
+ finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend
- @counts[cache_key][state]
+ Gitlab::IssuablesCountForState.new(finder)[state]
end
def close_issuable_url(issuable)
@@ -298,18 +302,6 @@ module IssuablesHelper
cookies[:collapsed_gutter] == 'true'
end
- def base_issuable_scope(issuable)
- issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable))
- end
-
- def issuable_state_scope(issuable)
- if issuable.respond_to?(:merged?) && issuable.merged?
- :merged
- else
- issuable.open? ? :opened : :closed
- end
- end
-
def issuable_templates(issuable)
@issuable_templates ||=
case issuable
@@ -329,7 +321,7 @@ module IssuablesHelper
end
def selected_template(issuable)
- params[:issuable_template] if issuable_templates(issuable).any?{ |template| template[:name] == params[:issuable_template] }
+ params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
end
def issuable_todo_button_data(issuable, todo, is_collapsed)
@@ -355,9 +347,19 @@ module IssuablesHelper
end
end
+ def labels_path
+ if @project
+ project_labels_path(@project)
+ elsif @group
+ group_labels_path(@group)
+ end
+ end
+
def issuable_sidebar_options(issuable, can_edit_issuable)
{
endpoint: "#{issuable_json_path(issuable)}?basic=true",
+ moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
+ projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
editable: can_edit_issuable,
currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url),
rootPath: root_path,
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 7e1ccb23e9e..212cdbb8157 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -47,13 +47,6 @@ module IssuesHelper
end
end
- def bulk_update_milestone_options
- milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
- milestones.unshift(Milestone::None)
-
- options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id])
- end
-
def milestone_options(object)
milestones = object.project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(object.milestone) if object.milestone.present? && object.milestone.closed?
@@ -63,7 +56,7 @@ module IssuesHelper
end
def project_options(issuable, current_user, ability: :read_project)
- projects = current_user.authorized_projects
+ projects = current_user.authorized_projects.order_id_desc
projects = projects.select do |project|
current_user.can?(ability, project)
end
@@ -93,14 +86,6 @@ module IssuesHelper
return 'hidden' if issue.closed? == closed
end
- def merge_requests_sentence(merge_requests)
- # Sorting based on the `!123` or `group/project!123` reference will sort
- # local merge requests first.
- merge_requests.map do |merge_request|
- merge_request.to_reference(@project)
- end.sort.to_sentence(last_word_connector: ', or ')
- end
-
def confidential_icon(issue)
icon('eye-slash') if issue.confidential?
end
@@ -137,7 +122,7 @@ module IssuesHelper
end
def awards_sort(awards)
- awards.sort_by do |award, notes|
+ awards.sort_by do |award, award_emojis|
if award == "thumbsup"
0
elsif award == "thumbsdown"
@@ -148,18 +133,6 @@ module IssuesHelper
end.to_h
end
- def due_date_options
- options = [
- Issue::AnyDueDate,
- Issue::NoDueDate,
- Issue::DueThisWeek,
- Issue::DueThisMonth,
- Issue::Overdue
- ]
-
- options_from_collection_for_select(options, 'name', 'title', params[:due_date])
- end
-
def link_to_discussions_to_resolve(merge_request, single_discussion = nil)
link_text = merge_request.to_reference
link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 4b99de1b6a5..e1ba7898ee6 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -43,11 +43,11 @@ module LabelsHelper
def label_filter_path(subject, label, type: :issue)
case subject
when Group
- send("#{type.to_s.pluralize}_group_path",
+ send("#{type.to_s.pluralize}_group_path", # rubocop:disable GitlabSecurity/PublicSend
subject,
label_name: [label.name])
when Project
- send("namespace_project_#{type.to_s.pluralize}_path",
+ send("namespace_project_#{type.to_s.pluralize}_path", # rubocop:disable GitlabSecurity/PublicSend
subject.namespace,
subject,
label_name: [label.name])
@@ -121,13 +121,14 @@ module LabelsHelper
end
end
- def labels_filter_path
- return group_labels_path(@group, :json) if @group
-
+ def labels_filter_path(only_group_labels = false)
project = @target_project || @project
if project
project_labels_path(project, :json)
+ elsif @group
+ options = { only_group_labels: only_group_labels } if only_group_labels
+ group_labels_path(@group, :json, options)
else
dashboard_labels_path(:json)
end
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 941cfce8370..46bced00c72 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -21,25 +21,28 @@ module MarkupHelper
end
# Use this in places where you would normally use link_to(gfm(...), ...).
- #
+ def link_to_markdown(body, url, html_options = {})
+ return '' if body.blank?
+
+ link_to_html(markdown(body, pipeline: :single_line), url, html_options)
+ end
+
+ def link_to_markdown_field(object, field, url, html_options = {})
+ rendered_field = markdown_field(object, field)
+
+ link_to_html(rendered_field, url, html_options)
+ end
+
# It solves a problem occurring with nested links (i.e.
# "<a>outer text <a>gfm ref</a> more outer text</a>"). This will not be
# interpreted as intended. Browsers will parse something like
# "<a>outer text </a><a>gfm ref</a> more outer text" (notice the last part is
- # not linked any more). link_to_gfm corrects that. It wraps all parts to
+ # not linked any more). link_to_html corrects that. It wraps all parts to
# explicitly produce the correct linking behavior (i.e.
# "<a>outer text </a><a>gfm ref</a><a> more outer text</a>").
- def link_to_gfm(body, url, html_options = {})
- return '' if body.blank?
+ def link_to_html(redacted, url, html_options = {})
+ fragment = Nokogiri::HTML::DocumentFragment.parse(redacted)
- context = {
- project: @project,
- current_user: (current_user if defined?(current_user)),
- pipeline: :single_line
- }
- gfm_body = Banzai.render(body, context)
-
- fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a'
# Fragment has only one node, and it's a link generated by `gfm`.
# Replace it with our requested link.
@@ -82,7 +85,10 @@ module MarkupHelper
def markdown_field(object, field)
object = object.for_display if object.respond_to?(:for_display)
+ redacted_field_html = object.try(:"redacted_#{field}_html")
+
return '' unless object.present?
+ return redacted_field_html if redacted_field_html
html = Banzai.render_field(object, field)
prepare_for_rendering(html, object.banzai_render_context(field))
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index f8860bfee99..446a59030a6 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -32,7 +32,18 @@ module MilestonesHelper
end
def milestone_issues_by_label_count(milestone, label, state:)
- milestone.issues.with_label(label.title).send(state).size
+ issues = milestone.issues.with_label(label.title)
+ issues =
+ case state
+ when :opened
+ issues.opened
+ when :closed
+ issues.closed
+ else
+ raise ArgumentError, "invalid milestone state `#{state}`"
+ end
+
+ issues.size
end
# Returns count of milestones for different states
@@ -153,7 +164,7 @@ module MilestonesHelper
def group_milestone_route(milestone, params = {})
params = nil if params.empty?
- if milestone.is_legacy_group_milestone?
+ if milestone.legacy_group_milestone?
group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: params)
else
group_milestone_path(@group, milestone.iid, milestone: params)
diff --git a/app/helpers/milestones_routing_helper.rb b/app/helpers/milestones_routing_helper.rb
index 766d5262018..a0b2616f224 100644
--- a/app/helpers/milestones_routing_helper.rb
+++ b/app/helpers/milestones_routing_helper.rb
@@ -1,16 +1,16 @@
module MilestonesRoutingHelper
def milestone_path(milestone, *args)
- if milestone.is_group_milestone?
+ if milestone.group_milestone?
group_milestone_path(milestone.group, milestone, *args)
- elsif milestone.is_project_milestone?
+ elsif milestone.project_milestone?
project_milestone_path(milestone.project, milestone, *args)
end
end
def milestone_url(milestone, *args)
- if milestone.is_group_milestone?
+ if milestone.group_milestone?
group_milestone_url(milestone.group, milestone, *args)
- elsif milestone.is_project_milestone?
+ elsif milestone.project_milestone?
project_milestone_url(milestone.project, milestone, *args)
end
end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 7f656b8caae..d7df9bb06d2 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -4,7 +4,8 @@ module NamespacesHelper
end
def namespaces_options(selected = :current_user, display_path: false, extra_group: nil)
- groups = current_user.owned_groups + current_user.masters_groups
+ groups = current_user.owned_groups + current_user.masters_groups
+ users = [current_user.namespace]
unless extra_group.nil? || extra_group.is_a?(Group)
extra_group = Group.find(extra_group) if Namespace.find(extra_group).kind == 'group'
@@ -14,22 +15,9 @@ module NamespacesHelper
groups |= [extra_group]
end
- users = [current_user.namespace]
-
- data_attr_group = { 'data-options-parent' => 'groups' }
- data_attr_users = { 'data-options-parent' => 'users' }
-
- group_opts = [
- "Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.full_path : g.human_name, g.id, data_attr_group] }
- ]
-
- users_opts = [
- "Users", users.sort_by(&:human_name).map { |u| [display_path ? u.path : u.human_name, u.id, data_attr_users] }
- ]
-
options = []
- options << group_opts
- options << users_opts
+ options << options_for_group(groups, display_path: display_path, type: 'group')
+ options << options_for_group(users, display_path: display_path, type: 'user')
if selected == :current_user && current_user.namespace
selected = current_user.namespace.id
@@ -45,4 +33,23 @@ module NamespacesHelper
avatar_icon(namespace.owner.email, size)
end
end
+
+ private
+
+ def options_for_group(namespaces, display_path:, type:)
+ group_label = type.pluralize
+ elements = namespaces.sort_by(&:human_name).map! do |n|
+ [display_path ? n.full_path : n.human_name, n.id,
+ data: {
+ options_parent: group_label,
+ visibility_level: n.visibility_level_value,
+ visibility: n.visibility,
+ name: n.name,
+ show_path: (type == 'group') ? group_path(n) : user_path(n),
+ edit_path: (type == 'group') ? edit_group_path(n) : nil
+ }]
+ end
+
+ [group_label.camelize, elements]
+ end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index b63b3b70903..a23a43c9f43 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -1,8 +1,8 @@
module NavHelper
def page_with_sidebar_class
class_name = page_gutter_class
- class_name << 'page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar
- class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @new_sidebar
+ class_name << 'page-with-new-sidebar' if defined?(@left_sidebar) && @left_sidebar
+ class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
class_name
end
@@ -30,24 +30,15 @@ module NavHelper
end
end
- def nav_header_class
- class_names = []
- class_names << 'with-horizontal-nav' if defined?(nav) && nav
-
- class_names
+ def nav_control_class
+ "nav-control" if current_user
end
- def layout_nav_class
- return [] if show_new_nav?
-
+ def user_dropdown_class
class_names = []
- class_names << 'page-with-layout-nav' if defined?(nav) && nav
- class_names << 'page-with-sub-nav' if content_for?(:sub_nav)
+ class_names << 'header-user-dropdown-toggle'
+ class_names << 'impersonated-user' if session[:impersonator_id]
class_names
end
-
- def nav_control_class
- "nav-control" if current_user
- end
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index e857e837c16..ce028195e51 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -73,7 +73,7 @@ module NotesHelper
end
def note_max_access_for_user(note)
- note.project.team.human_max_access(note.author_id)
+ note.project.team.max_member_access(note.author_id)
end
def discussion_path(discussion)
@@ -93,11 +93,13 @@ module NotesHelper
end
end
- def notes_url
+ def notes_url(params = {})
if @snippet.is_a?(PersonalSnippet)
- snippet_notes_path(@snippet)
+ snippet_notes_path(@snippet, params)
else
- project_noteable_notes_path(@project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)
+ params.merge!(target_id: @noteable.id, target_type: @noteable.class.name.underscore)
+
+ project_noteable_notes_path(@project, params)
end
end
@@ -144,4 +146,8 @@ module NotesHelper
autocomplete: autocomplete
}
end
+
+ def discussion_resolved_intro(discussion)
+ discussion.resolved_by_push? ? 'Automatically resolved' : 'Resolved'
+ end
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index b30b2eb1d03..5946c475835 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -4,7 +4,7 @@ module PageLayoutHelper
@page_title.push(*titles.compact) if titles.any?
- if show_new_nav? && titles.any? && !defined?(@breadcrumb_title)
+ if titles.any? && !defined?(@breadcrumb_title)
@breadcrumb_title = @page_title.last
end
@@ -80,7 +80,9 @@ module PageLayoutHelper
@header_title = title
@header_title_url = title_url
else
- @header_title_url ? link_to(@header_title, @header_title_url) : @header_title
+ return @header_title unless @header_title_url
+
+ breadcrumb_list_item(link_to(@header_title, @header_title_url))
end
end
diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb
new file mode 100644
index 00000000000..83dd76a01dd
--- /dev/null
+++ b/app/helpers/pagination_helper.rb
@@ -0,0 +1,21 @@
+module PaginationHelper
+ def paginate_collection(collection, remote: nil)
+ if collection.is_a?(Kaminari::PaginatableWithoutCount)
+ paginate_without_count(collection)
+ elsif collection.respond_to?(:total_pages)
+ paginate_with_count(collection, remote: remote)
+ end
+ end
+
+ def paginate_without_count(collection)
+ render(
+ 'kaminari/gitlab/without_count',
+ previous_path: path_to_prev_page(collection),
+ next_path: path_to_next_page(collection)
+ )
+ end
+
+ def paginate_with_count(collection, remote: nil)
+ paginate(collection, remote: remote, theme: 'gitlab')
+ end
+end
diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb
index fee1edc2a1b..6edaf78de1b 100644
--- a/app/helpers/pipeline_schedules_helper.rb
+++ b/app/helpers/pipeline_schedules_helper.rb
@@ -1,10 +1,10 @@
module PipelineSchedulesHelper
def timezone_data
- ActiveSupport::TimeZone.all.map do |timezone|
- {
- name: timezone.name,
- offset: timezone.utc_offset,
- identifier: timezone.tzinfo.identifier
+ ActiveSupport::TimeZone.all.map do |timezone|
+ {
+ name: timezone.name,
+ offset: timezone.utc_offset,
+ identifier: timezone.tzinfo.identifier
}
end
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index d36bb4ab074..0d7347ed30d 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -40,6 +40,10 @@ module PreferencesHelper
]
end
+ def user_application_theme
+ @user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class
+ end
+
def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class
end
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index 45238f12ac7..5a4fda0724c 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -1,7 +1,12 @@
module ProfilesHelper
- def email_provider_label
- return unless current_user.external_email?
-
- current_user.email_provider.present? ? Gitlab::OAuth::Provider.label_for(current_user.email_provider) : "LDAP"
+ def attribute_provider_label(attribute)
+ user_synced_attributes_metadata = current_user.user_synced_attributes_metadata
+ if user_synced_attributes_metadata&.synced?(attribute)
+ if user_synced_attributes_metadata.provider
+ Gitlab::OAuth::Provider.label_for(user_synced_attributes_metadata.provider)
+ else
+ 'LDAP'
+ end
+ end
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index a268413e84f..c0114dd0256 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -1,4 +1,6 @@
module ProjectsHelper
+ include Gitlab::CurrentSettings
+
def link_to_project(project)
link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do
title = content_tag(:span, project.name, class: 'project-name')
@@ -13,9 +15,13 @@ module ProjectsHelper
end
def link_to_member_avatar(author, opts = {})
- default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
+ default_opts = { size: 16 }
opts = default_opts.merge(opts)
- image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt: '') if opts[:avatar]
+
+ 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: '')
end
def link_to_member(project, author, opts = {}, &block)
@@ -27,7 +33,7 @@ module ProjectsHelper
author_html = ""
# Build avatar image tag
- author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar]
+ author_html << link_to_member_avatar(author, opts) if opts[:avatar]
# Build name span tag
if opts[:by_username]
@@ -52,35 +58,32 @@ module ProjectsHelper
def project_title(project)
namespace_link =
if project.group
- group_title(project.group)
+ group_title(project.group, nil, nil)
else
owner = project.namespace.owner
link_to(simple_sanitize(owner.name), user_path(owner))
end
- project_link = link_to project_path(project), { class: "project-item-select-holder" } do
+ project_link = link_to project_path(project) do
output =
- if show_new_nav?
- project_icon(project, alt: project.name, class: 'avatar-tile', width: 16, height: 16)
+ if project.avatar_url && !Rails.env.test?
+ project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15)
else
""
end
- output << simple_sanitize(project.name)
+ output << content_tag("span", simple_sanitize(project.name), class: "breadcrumb-item-text js-breadcrumb-item-text")
output.html_safe
end
- if current_user
- project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do
- icon("chevron-down")
- end
- end
+ namespace_link = breadcrumb_list_item(namespace_link) unless project.group
+ project_link = breadcrumb_list_item project_link
- "#{namespace_link} / #{project_link}".html_safe
+ "#{namespace_link} #{project_link}".html_safe
end
def remove_project_message(project)
- _("You are going to remove %{project_name_with_namespace}.\nRemoved project CANNOT be restored!\nAre you ABSOLUTELY sure?") %
+ _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
{ project_name_with_namespace: project.name_with_namespace }
end
@@ -149,15 +152,16 @@ module ProjectsHelper
# Don't show option "everyone with access" if project is private
options = project_feature_options
+ level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend
+
if @project.private?
- level = @project.project_feature.send(field)
disabled_option = ProjectFeature::ENABLED
highest_available_option = ProjectFeature::PRIVATE if level == disabled_option
end
options = options_for_select(
options.invert,
- selected: highest_available_option || @project.project_feature.public_send(field),
+ selected: highest_available_option || level,
disabled: disabled_option
)
@@ -234,6 +238,8 @@ module ProjectsHelper
# If no limit is applied we'll just issue a COUNT since the result set could
# be too large to load into memory.
def any_projects?(projects)
+ return projects.any? if projects.is_a?(Array)
+
if projects.limit_value
projects.to_a.any?
else
@@ -486,7 +492,7 @@ module ProjectsHelper
end
def filename_path(project, filename)
- if project && blob = project.repository.send(filename)
+ if project && blob = project.repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend
project_blob_path(
project,
tree_join(project.default_branch, blob.name)
@@ -539,6 +545,43 @@ module ProjectsHelper
current_application_settings.restricted_visibility_levels || []
end
+ def project_permissions_settings(project)
+ feature = project.project_feature
+ {
+ visibilityLevel: project.visibility_level,
+ requestAccessEnabled: !!project.request_access_enabled,
+ issuesAccessLevel: feature.issues_access_level,
+ repositoryAccessLevel: feature.repository_access_level,
+ mergeRequestsAccessLevel: feature.merge_requests_access_level,
+ buildsAccessLevel: feature.builds_access_level,
+ wikiAccessLevel: feature.wiki_access_level,
+ snippetsAccessLevel: feature.snippets_access_level,
+ containerRegistryEnabled: !!project.container_registry_enabled,
+ lfsEnabled: !!project.lfs_enabled
+ }
+ end
+
+ def project_permissions_panel_data(project)
+ data = {
+ currentSettings: project_permissions_settings(project),
+ canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
+ allowedVisibilityOptions: project_allowed_visibility_levels(project),
+ visibilityHelpPath: help_page_path('public_access/public_access'),
+ registryAvailable: Gitlab.config.registry.enabled,
+ registryHelpPath: help_page_path('user/project/container_registry'),
+ lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?,
+ lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ }
+
+ data.to_json.html_safe
+ end
+
+ def project_allowed_visibility_levels(project)
+ Gitlab::VisibilityLevel.values.select do |level|
+ project.visibility_level_allowed?(level) && !restricted_levels.include?(level)
+ end
+ end
+
def find_file_path
return unless @project && !@project.empty_repo?
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index ae0e0aa3cf9..cf28a917fd1 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -10,6 +10,7 @@ module SearchHelper
search_pattern = Regexp.new(Regexp.escape(term), "i")
generic_results = project_autocomplete + default_autocomplete + help_autocomplete
+ generic_results.concat(default_autocomplete_admin) if current_user.admin?
generic_results.select! { |result| result[:label] =~ search_pattern }
[
@@ -41,8 +42,14 @@ module SearchHelper
[
{ category: "Settings", label: "User settings", url: profile_path },
{ category: "Settings", label: "SSH Keys", url: profile_keys_path },
- { category: "Settings", label: "Dashboard", url: root_path },
- { category: "Settings", label: "Admin Section", url: admin_root_path }
+ { category: "Settings", label: "Dashboard", url: root_path }
+ ]
+ end
+
+ # Autocomplete results for settings pages, for admins
+ def default_autocomplete_admin
+ [
+ { category: "Settings", label: "Admin Section", url: admin_root_path }
]
end
@@ -85,7 +92,7 @@ module SearchHelper
# Autocomplete results for the current user's groups
def groups_autocomplete(term, limit = 5)
- current_user.authorized_groups.search(term).limit(limit).map do |group|
+ current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group|
{
category: "Groups",
id: group.id,
@@ -97,7 +104,7 @@ module SearchHelper
# Autocomplete results for the current user's projects
def projects_autocomplete(term, limit = 5)
- current_user.authorized_projects.search_by_title(term)
+ current_user.authorized_projects.order_id_desc.search_by_title(term)
.sorted_by_stars.non_archived.limit(limit).map do |p|
{
category: "Projects",
@@ -127,19 +134,21 @@ module SearchHelper
end
def search_filter_input_options(type)
- opts = {
- id: "filtered-search-#{type}",
- placeholder: 'Search or filter results...',
- data: {
- 'username-params' => @users.to_json(only: [:id, :username])
+ opts =
+ {
+ id: "filtered-search-#{type}",
+ placeholder: 'Search or filter results...',
+ data: {
+ 'username-params' => @users.to_json(only: [:id, :username])
+ }
}
- }
if @project.present?
opts[:data]['project-id'] = @project.id
opts[:data]['base-endpoint'] = project_path(@project)
else
# Group context
+ opts[:data]['group-id'] = @group.id
opts[:data]['base-endpoint'] = group_canonical_path(@group)
end
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index 08fd97cd048..c98f65c7644 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -22,8 +22,14 @@ module SystemNoteHelper
'duplicate' => 'icon_clone'
}.freeze
+ def system_note_icon_name(note)
+ ICON_NAMES_BY_ACTION[note.system_note_metadata&.action]
+ end
+
def icon_for_system_note(note)
- icon_name = ICON_NAMES_BY_ACTION[note.system_note_metadata&.action]
+ icon_name = system_note_icon_name(note)
custom_icon(icon_name) if icon_name
end
+
+ extend self
end
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index ee701076a14..3308ab0c259 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -119,4 +119,8 @@ 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 e0d3e9b88f3..95dbdc8ec46 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -99,7 +99,9 @@ module TreeHelper
end
# returns the relative path of the first subdir that doesn't have only one directory descendant
- def flatten_tree(tree)
+ def flatten_tree(root_path, tree)
+ return tree.flat_path.sub(/\A#{root_path}\//, '') if tree.flat_path.present?
+
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))
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index 3b175251446..456598b4c28 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -2,7 +2,7 @@ module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
image_url = VersionCheck.new.url
- image_tag image_url, class: 'js-version-status-badge', lazy: false
+ image_tag image_url, class: 'js-version-status-badge'
end
end
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index 35755bc149b..46867d2d974 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -63,6 +63,68 @@ module VisibilityLevelHelper
end
end
+ def restricted_visibility_level_description(level)
+ level_name = Gitlab::VisibilityLevel.level_name(level)
+ "#{level_name.capitalize} visibility has been restricted by the administrator."
+ end
+
+ def disallowed_visibility_level_description(level, form_model)
+ case form_model
+ when Project
+ disallowed_project_visibility_level_description(level, form_model)
+ when Group
+ disallowed_group_visibility_level_description(level, form_model)
+ end
+ end
+
+ # Note: these messages closely mirror the form validation strings found in the project
+ # model and any changes or additons to these may also need to be made there.
+ def disallowed_project_visibility_level_description(level, project)
+ level_name = Gitlab::VisibilityLevel.level_name(level).downcase
+ reasons = []
+ instructions = ''
+
+ unless project.visibility_level_allowed_as_fork?(level)
+ reasons << "the fork source project has lower visibility"
+ end
+
+ unless project.visibility_level_allowed_by_group?(level)
+ errors = visibility_level_errors_for_group(project.group, level_name)
+
+ reasons << errors[:reason]
+ instructions << errors[:instruction]
+ end
+
+ reasons = reasons.any? ? ' because ' + reasons.to_sentence : ''
+ "This project cannot be #{level_name}#{reasons}.#{instructions}".html_safe
+ end
+
+ # Note: these messages closely mirror the form validation strings found in the group
+ # model and any changes or additons to these may also need to be made there.
+ def disallowed_group_visibility_level_description(level, group)
+ level_name = Gitlab::VisibilityLevel.level_name(level).downcase
+ reasons = []
+ instructions = ''
+
+ unless group.visibility_level_allowed_by_projects?(level)
+ reasons << "it contains projects with higher visibility"
+ end
+
+ unless group.visibility_level_allowed_by_sub_groups?(level)
+ reasons << "it contains sub-groups with higher visibility"
+ end
+
+ unless group.visibility_level_allowed_by_parent?(level)
+ errors = visibility_level_errors_for_group(group.parent, level_name)
+
+ reasons << errors[:reason]
+ instructions << errors[:instruction]
+ end
+
+ reasons = reasons.any? ? ' because ' + reasons.to_sentence : ''
+ "This group cannot be #{level_name}#{reasons}.#{instructions}".html_safe
+ end
+
def visibility_icon_description(form_model)
case form_model
when Project
@@ -95,7 +157,18 @@ module VisibilityLevelHelper
:default_group_visibility,
to: :current_application_settings
- def skip_level?(form_model, level)
- form_model.is_a?(Project) && !form_model.visibility_level_allowed?(level)
+ def disallowed_visibility_level?(form_model, level)
+ return false unless form_model.respond_to?(:visibility_level_allowed?)
+ !form_model.visibility_level_allowed?(level)
+ end
+
+ private
+
+ def visibility_level_errors_for_group(group, level_name)
+ group_name = link_to group.name, group_path(group)
+ change_visiblity = link_to 'change the visibility', edit_group_path(group)
+
+ { reason: "the visibility of #{group_name} is #{group.visibility}",
+ instruction: " To make this group #{level_name}, you must first #{change_visiblity} of the parent group." }
end
end
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index 99212a3438f..815fab9e061 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -10,4 +10,15 @@ module WikiHelper
.map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize }
.join(' / ')
end
+
+ def wiki_breadcrumb_dropdown_links(page_slug)
+ page_slug_split = page_slug.split('/')
+ page_slug_split.pop(1)
+ current_slug = ""
+ page_slug_split
+ .map do |dir_or_page|
+ current_slug = "#{current_slug}#{dir_or_page}/"
+ add_to_breadcrumb_dropdown link_to(WikiPage.unhyphenize(dir_or_page).capitalize, project_wiki_path(@project, current_slug)), location: :after
+ end
+ end
end
diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index 654468bc7fe..8e99db444d6 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -1,11 +1,13 @@
class BaseMailer < ActionMailer::Base
+ include Gitlab::CurrentSettings
+
around_action :render_with_default_locale
helper ApplicationHelper
helper MarkupHelper
attr_accessor :current_user
- helper_method :current_user, :can?
+ helper_method :current_user, :can?, :current_application_settings
default from: proc { default_sender_address.format }
default reply_to: proc { default_reply_to_address.format }
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 7b617b359ea..d76c61c369f 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -11,11 +11,11 @@ module Emails
@member_source_type = member_source_type
@member_id = member_id
- admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
+ admins = member_source.members.owners_and_masters.pluck(:notification_email)
# A project in a group can have no explicit owners/masters, in that case
# we fallbacks to the group's owners/masters.
if admins.empty? && member_source.respond_to?(:group) && member_source.group
- admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email)
+ admins = member_source.group.members.owners_and_masters.pluck(:notification_email)
end
mail(to: admins,
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index f9c48482be7..ff15689ecac 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -8,7 +8,27 @@ class Appearance < ActiveRecord::Base
validates :logo, file_size: { maximum: 1.megabyte }
validates :header_logo, file_size: { maximum: 1.megabyte }
+ validate :single_appearance_row, on: :create
+
mount_uploader :logo, AttachmentUploader
mount_uploader :header_logo, AttachmentUploader
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
+ CACHE_KEY = 'current_appearance'.freeze
+
+ after_commit :flush_redis_cache
+
+ def self.current
+ Rails.cache.fetch(CACHE_KEY) { first }
+ end
+
+ def flush_redis_cache
+ Rails.cache.delete(CACHE_KEY)
+ end
+
+ def single_appearance_row
+ if self.class.any?
+ errors.add(:single_appearance_row, 'Only 1 appearances row can exist')
+ end
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index bd7c4cd45ea..3568e72e463 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -13,6 +13,11 @@ class ApplicationSetting < ActiveRecord::Base
[\r\n] # any number of newline characters
}x
+ # Setting a key restriction to `-1` means that all keys of this type are
+ # forbidden.
+ FORBIDDEN_KEY_VALUE = KeyRestrictionValidator::FORBIDDEN
+ SUPPORTED_KEY_TYPES = %i[rsa dsa ecdsa ed25519].freeze
+
serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize
serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize
serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiveRecordSerialize
@@ -146,6 +151,12 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { greater_than_or_equal_to: 0 }
+ SUPPORTED_KEY_TYPES.each do |type|
+ validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
+ end
+
+ validates :allowed_key_types, presence: true
+
validates_each :restricted_visibility_levels do |record, attr, value|
value&.each do |level|
unless Gitlab::VisibilityLevel.options.value?(level)
@@ -171,6 +182,7 @@ class ApplicationSetting < ActiveRecord::Base
end
before_validation :ensure_uuid!
+
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
@@ -221,6 +233,9 @@ class ApplicationSetting < ActiveRecord::Base
default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'],
disabled_oauth_sign_in_sources: [],
domain_whitelist: Settings.gitlab['domain_whitelist'],
+ dsa_key_restriction: 0,
+ ecdsa_key_restriction: 0,
+ ed25519_key_restriction: 0,
gravatar_enabled: Settings.gravatar['enabled'],
help_page_text: nil,
help_page_hide_commercial_content: false,
@@ -239,8 +254,10 @@ class ApplicationSetting < ActiveRecord::Base
max_attachment_size: Settings.gitlab['max_attachment_size'],
password_authentication_enabled: Settings.gitlab['password_authentication_enabled'],
performance_bar_allowed_group_id: nil,
+ rsa_key_restriction: 0,
plantuml_enabled: false,
plantuml_url: nil,
+ project_export_enabled: true,
recaptcha_enabled: false,
repository_checks_enabled: true,
repository_storages: ['default'],
@@ -412,6 +429,18 @@ class ApplicationSetting < ActiveRecord::Base
usage_ping_can_be_configured? && super
end
+ def allowed_key_types
+ SUPPORTED_KEY_TYPES.select do |type|
+ key_restriction_for(type) != FORBIDDEN_KEY_VALUE
+ end
+ end
+
+ def key_restriction_for(type)
+ attr_name = "#{type}_key_restriction"
+
+ has_attribute?(attr_name) ? public_send(attr_name) : FORBIDDEN_KEY_VALUE # rubocop:disable GitlabSecurity/PublicSend
+ end
+
private
def ensure_uuid!
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index 91b62dabbcd..4d1a15c53aa 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -17,6 +17,9 @@ class AwardEmoji < ActiveRecord::Base
scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) }
+ after_save :expire_etag_cache
+ after_destroy :expire_etag_cache
+
class << self
def votes_for_collection(ids, type)
select('name', 'awardable_id', 'COUNT(*) as count')
@@ -32,4 +35,8 @@ class AwardEmoji < ActiveRecord::Base
def upvote?
self.name == UPVOTE_NAME
end
+
+ def expire_etag_cache
+ awardable.try(:expire_etag_cache)
+ end
end
diff --git a/app/models/blob_viewer/composer_json.rb b/app/models/blob_viewer/composer_json.rb
index ef8b4aef8e8..def4879fbb5 100644
--- a/app/models/blob_viewer/composer_json.rb
+++ b/app/models/blob_viewer/composer_json.rb
@@ -9,7 +9,7 @@ module BlobViewer
end
def manager_url
- 'https://getcomposer.com/'
+ 'https://getcomposer.org/'
end
def package_name
diff --git a/app/models/blob_viewer/notebook.rb b/app/models/blob_viewer/notebook.rb
index 8632b8a9885..e00b47e6c17 100644
--- a/app/models/blob_viewer/notebook.rb
+++ b/app/models/blob_viewer/notebook.rb
@@ -2,7 +2,7 @@ module BlobViewer
class Notebook < Base
include Rich
include ClientSide
-
+
self.partial_name = 'notebook'
self.extensions = %w(ipynb)
self.binary = false
diff --git a/app/models/board.rb b/app/models/board.rb
index 97d0f550925..5bb7d3d3722 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -3,7 +3,19 @@ class Board < ActiveRecord::Base
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
- validates :project, presence: true
+ validates :project, presence: true, if: :project_needed?
+
+ def project_needed?
+ true
+ end
+
+ def parent
+ project
+ end
+
+ def group_board?
+ false
+ end
def backlog_list
lists.merge(List.backlog).take
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 944725d91c3..0b561203914 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -14,10 +14,26 @@ class BroadcastMessage < ActiveRecord::Base
default_value_for :color, '#E75E40'
default_value_for :font, '#FFFFFF'
+ CACHE_KEY = 'broadcast_message_current'.freeze
+
+ after_commit :flush_redis_cache
+
def self.current
- Rails.cache.fetch("broadcast_message_current", expires_in: 1.minute) do
- where('ends_at > :now AND starts_at <= :now', now: Time.zone.now).order([:created_at, :id]).to_a
- end
+ messages = Rails.cache.fetch(CACHE_KEY) { current_and_future_messages.to_a }
+
+ return messages if messages.empty?
+
+ now_or_future = messages.select(&:now_or_future?)
+
+ # If there are cached entries but none are to be displayed we'll purge the
+ # cache so we don't keep running this code all the time.
+ Rails.cache.delete(CACHE_KEY) if now_or_future.empty?
+
+ now_or_future.select(&:now?)
+ end
+
+ def self.current_and_future_messages
+ where('ends_at > :now', now: Time.zone.now).order_id_asc
end
def active?
@@ -31,4 +47,20 @@ class BroadcastMessage < ActiveRecord::Base
def ended?
ends_at < Time.zone.now
end
+
+ def now?
+ (starts_at..ends_at).cover?(Time.zone.now)
+ end
+
+ def future?
+ starts_at > Time.zone.now
+ end
+
+ def now_or_future?
+ now? || future?
+ end
+
+ def flush_redis_cache
+ Rails.cache.delete(CACHE_KEY)
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 569a71d36c1..c41355f5aff 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -3,6 +3,7 @@ module Ci
include TokenAuthenticatable
include AfterCommitQueue
include Presentable
+ include Importable
belongs_to :runner
belongs_to :trigger_request
@@ -34,6 +35,7 @@ module Ci
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
+ scope :ref_protected, -> { where(protected: true) }
mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
@@ -46,7 +48,10 @@ module Ci
before_save :ensure_token
before_destroy { unscoped_project }
- after_create :execute_hooks
+ after_create do |build|
+ run_after_commit { BuildHooksWorker.perform_async(build.id) }
+ end
+
after_commit :update_project_statistics_after_save, on: [:create, :update]
after_commit :update_project_statistics, on: :destroy
@@ -194,10 +199,7 @@ module Ci
# * Maximum length is 63 bytes
# * First/Last Character is not a hyphen
def ref_slug
- ref.to_s
- .downcase
- .gsub(/[^a-z0-9]/, '-')[0..62]
- .gsub(/(\A-+|-+\z)/, '')
+ Gitlab::Utils.slugify(ref.to_s)
end
# Variables whose value does not depend on environment
@@ -214,6 +216,7 @@ module Ci
variables += runner.predefined_variables if runner
variables += project.container_registry_variables
variables += project.deployment_variables if has_environment?
+ variables += project.auto_devops_variables
variables += yaml_variables
variables += user_variables
variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group
@@ -397,7 +400,9 @@ module Ci
[
{ key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
- { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }
+ { key: 'GITLAB_USER_EMAIL', value: user.email, public: true },
+ { key: 'GITLAB_USER_LOGIN', value: user.username, public: true },
+ { key: 'GITLAB_USER_NAME', value: user.name, public: true }
]
end
@@ -456,6 +461,10 @@ module Ci
trace
end
+ def serializable_hash(options = {})
+ super(options).merge(when: read_attribute(:when))
+ end
+
private
def update_artifacts_size
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index ea7331cb27f..871c76fbad3 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -38,6 +38,7 @@ module Ci
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
+ after_initialize :set_config_source, if: :new_record?
after_create :keep_around_commits, unless: :importing?
enum source: {
@@ -50,6 +51,12 @@ module Ci
external: 6
}
+ enum config_source: {
+ unknown_source: nil,
+ repository_source: 1,
+ auto_devops_source: 2
+ }
+
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
@@ -304,6 +311,10 @@ module Ci
@stage_seeds ||= config_processor.stage_seeds(self)
end
+ def has_kubernetes_active?
+ project.kubernetes_service&.active?
+ end
+
def has_stage_seeds?
stage_seeds.any?
end
@@ -312,6 +323,14 @@ module Ci
builds.latest.failed_but_allowed.any?
end
+ def set_config_source
+ if ci_yaml_from_repo
+ self.config_source = :repository_source
+ elsif implied_ci_yaml_file
+ self.config_source = :auto_devops_source
+ end
+ end
+
def config_processor
return unless ci_yaml_file
return @config_processor if defined?(@config_processor)
@@ -338,11 +357,17 @@ module Ci
def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file)
- @ci_yaml_file = begin
- project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
- rescue Rugged::ReferenceError, GRPC::NotFound, GRPC::Internal
- self.yaml_errors =
- "Failed to load CI/CD config file at #{ci_yaml_file_path}"
+ @ci_yaml_file =
+ if auto_devops_source?
+ implied_ci_yaml_file
+ else
+ ci_yaml_from_repo
+ end
+
+ if @ci_yaml_file
+ @ci_yaml_file
+ else
+ self.yaml_errors = "Failed to load CI/CD config file for #{sha}"
nil
end
end
@@ -393,7 +418,8 @@ module Ci
def predefined_variables
[
{ key: 'CI_PIPELINE_ID', value: id.to_s, public: true },
- { key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true }
+ { key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true },
+ { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
]
end
@@ -429,6 +455,23 @@ module Ci
private
+ def ci_yaml_from_repo
+ return unless project
+ return unless sha
+
+ project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
+ rescue GRPC::NotFound, Rugged::ReferenceError, GRPC::Internal
+ nil
+ end
+
+ def implied_ci_yaml_file
+ return unless project
+
+ if project.auto_devops_enabled?
+ Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
+ end
+ end
+
def pipeline_data
Gitlab::DataBuilder::Pipeline.build(self)
end
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 085eeeae157..e7e02587759 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -9,7 +9,7 @@ module Ci
belongs_to :owner, class_name: 'User'
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines
- has_many :variables, class_name: 'Ci::PipelineScheduleVariable'
+ has_many :variables, class_name: 'Ci::PipelineScheduleVariable', validate: false
validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb
index 1ff177616e8..ee5b8733fac 100644
--- a/app/models/ci/pipeline_schedule_variable.rb
+++ b/app/models/ci/pipeline_schedule_variable.rb
@@ -4,5 +4,7 @@ module Ci
include HasVariable
belongs_to :pipeline_schedule
+
+ validates :key, uniqueness: { scope: :pipeline_schedule_id }
end
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index c6d23898560..b1798084787 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -5,7 +5,7 @@ module Ci
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
- FORM_EDITABLE = %i[description tag_list active run_untagged locked].freeze
+ FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level].freeze
has_many :builds
has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -35,11 +35,17 @@ module Ci
end
validate :tag_constraints
+ validates :access_level, presence: true
acts_as_taggable
after_destroy :cleanup_runner_queue
+ enum access_level: {
+ not_protected: 0,
+ ref_protected: 1
+ }
+
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
@@ -106,6 +112,8 @@ module Ci
end
def can_pick?(build)
+ return false if self.ref_protected? && !build.protected?
+
assignable_for?(build.project) && accepting_tags?(build)
end
@@ -142,7 +150,7 @@ module Ci
expire: RUNNER_QUEUE_EXPIRY_TIME, overwrite: false)
end
- def is_runner_queue_value_latest?(value)
+ def runner_queue_value_latest?(value)
ensure_runner_queue_value == value if value.present?
end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 59570924c8d..754c37518b3 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -1,11 +1,70 @@
module Ci
class Stage < ActiveRecord::Base
extend Ci::Model
+ include Importable
+ include HasStatus
+ include Gitlab::OptimisticLocking
+
+ enum status: HasStatus::STATUSES_ENUM
belongs_to :project
belongs_to :pipeline
- has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
- has_many :builds, foreign_key: :commit_id
+ has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
+ has_many :builds, foreign_key: :stage_id
+
+ validates :project, presence: true, unless: :importing?
+ validates :pipeline, presence: true, unless: :importing?
+ validates :name, presence: true, unless: :importing?
+
+ after_initialize do |stage|
+ self.status = DEFAULT_STATUS if self.status.nil?
+ end
+
+ state_machine :status, initial: :created do
+ event :enqueue do
+ transition created: :pending
+ transition [:success, :failed, :canceled, :skipped] => :running
+ end
+
+ event :run do
+ transition any - [:running] => :running
+ end
+
+ event :skip do
+ transition any - [:skipped] => :skipped
+ end
+
+ event :drop do
+ transition any - [:failed] => :failed
+ end
+
+ event :succeed do
+ transition any - [:success] => :success
+ end
+
+ event :cancel do
+ transition any - [:canceled] => :canceled
+ end
+
+ event :block do
+ transition any - [:manual] => :manual
+ end
+ end
+
+ def update_status
+ retry_optimistic_lock(self) do
+ case statuses.latest.status
+ when 'pending' then enqueue
+ when 'running' then run
+ when 'success' then succeed
+ when 'failed' then drop
+ when 'canceled' then cancel
+ when 'manual' then block
+ when 'skipped' then skip
+ else skip
+ end
+ end
+ end
end
end
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index c58ce5c3717..2c860598281 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -6,6 +6,10 @@ module Ci
belongs_to :pipeline, foreign_key: :commit_id
has_many :builds
+ # We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables.
+ # Ci::TriggerRequest doesn't save variables anymore.
+ validates :variables, absence: true
+
serialize :variables # rubocop:disable Cop/ActiveRecordSerialize
def user_variables
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 7940733f557..2ae8890c1b3 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -16,6 +16,8 @@ class Commit
participant :notes_with_associations
attr_accessor :project, :author
+ attr_accessor :redacted_description_html
+ attr_accessor :redacted_title_html
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
@@ -26,6 +28,13 @@ class Commit
# The SHA can be between 7 and 40 hex characters.
COMMIT_SHA_PATTERN = '\h{7,40}'.freeze
+ def banzai_render_context(field)
+ context = { pipeline: :single_line, project: self.project }
+ context[:author] = self.author if self.author
+
+ context
+ end
+
class << self
def decorate(commits, project)
commits.map do |commit|
@@ -55,7 +64,8 @@ class Commit
end
def from_hash(hash, project)
- new(Gitlab::Git::Commit.new(hash), project)
+ raw_commit = Gitlab::Git::Commit.new(project.repository.raw, hash)
+ new(raw_commit, project)
end
def valid_hash?(key)
@@ -199,7 +209,7 @@ class Commit
end
def method_missing(m, *args, &block)
- @raw.send(m, *args, &block)
+ @raw.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def respond_to_missing?(method, include_private = false)
@@ -250,6 +260,28 @@ class Commit
project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
end
+ def cherry_pick_description(user)
+ message_body = "(cherry picked from commit #{sha})"
+
+ if merged_merge_request?(user)
+ commits_in_merge_request = merged_merge_request(user).commits
+
+ if commits_in_merge_request.present?
+ message_body << "\n"
+
+ commits_in_merge_request.reverse.each do |commit_in_merge|
+ message_body << "\n#{commit_in_merge.short_id} #{commit_in_merge.title}"
+ end
+ end
+ end
+
+ message_body
+ end
+
+ def cherry_pick_message(user)
+ %Q{#{message}\n\n#{cherry_pick_description(user)}}
+ end
+
def revert_description(user)
if merged_merge_request?(user)
"This reverts merge request #{merged_merge_request(user).to_reference}"
@@ -320,21 +352,11 @@ class Commit
end
def raw_diffs(*args)
- if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
- Gitlab::GitalyClient::CommitService.new(project.repository).diff_from_parent(self, *args)
- else
- raw.diffs(*args)
- end
+ raw.diffs(*args)
end
def raw_deltas
- @deltas ||= Gitlab::GitalyClient.migrate(:commit_deltas) do |is_enabled|
- if is_enabled
- Gitlab::GitalyClient::CommitService.new(project.repository).commit_deltas(self)
- else
- raw.deltas
- end
- end
+ @deltas ||= raw.deltas
end
def diffs(diff_options = nil)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 07cec63b939..f3888528940 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -38,15 +38,23 @@ class CommitStatus < ActiveRecord::Base
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
- state_machine :status do
- event :enqueue do
- transition [:created, :skipped, :manual] => :pending
- end
+ enum failure_reason: {
+ unknown_failure: nil,
+ script_failure: 1,
+ api_failure: 2,
+ stuck_or_timeout_failure: 3,
+ runner_system_failure: 4
+ }
+ state_machine :status do
event :process do
transition [:skipped, :manual] => :created
end
+ event :enqueue do
+ transition [:created, :skipped, :manual] => :pending
+ end
+
event :run do
transition pending: :running
end
@@ -79,6 +87,11 @@ class CommitStatus < ActiveRecord::Base
commit_status.finished_at = Time.now
end
+ before_transition any => :failed do |commit_status, transition|
+ failure_reason = transition.args.first
+ commit_status.failure_reason = failure_reason
+ end
+
after_transition do |commit_status, transition|
next if transition.loopback?
@@ -91,6 +104,7 @@ class CommitStatus < ActiveRecord::Base
end
end
+ StageUpdateWorker.perform_async(commit_status.stage_id)
ExpireJobCacheWorker.perform_async(commit_status.id)
end
end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index f4f9b037957..9adc309a22b 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -11,6 +11,21 @@ module Awardable
end
module ClassMethods
+ def awarded(user, name)
+ sql = <<~EOL
+ EXISTS (
+ SELECT TRUE
+ FROM award_emoji
+ WHERE user_id = :user_id AND
+ name = :name AND
+ awardable_type = :awardable_type AND
+ awardable_id = #{self.arel_table.name}.id
+ )
+ EOL
+
+ where(sql, user_id: user.id, name: name, awardable_type: self.name)
+ end
+
def order_upvotes_desc
order_votes_desc(AwardEmoji::UPVOTE_NAME)
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 48547a938fc..193e459977a 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -78,7 +78,7 @@ module CacheMarkdownField
def cached_html_up_to_date?(markdown_field)
html_field = cached_markdown_fields.html_field(markdown_field)
- cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present?
+ cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend
return false unless cached
markdown_changed = attribute_changed?(markdown_field) || false
@@ -93,14 +93,14 @@ module CacheMarkdownField
end
def attribute_invalidated?(attr)
- __send__("#{attr}_invalidated?")
+ __send__("#{attr}_invalidated?") # rubocop:disable GitlabSecurity/PublicSend
end
def cached_html_for(markdown_field)
raise ArgumentError.new("Unknown field: #{field}") unless
cached_markdown_fields.markdown_fields.include?(markdown_field)
- __send__(cached_markdown_fields.html_field(markdown_field))
+ __send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end
included do
diff --git a/app/models/concerns/editable.rb b/app/models/concerns/editable.rb
index 28623d257a6..c0a3099f676 100644
--- a/app/models/concerns/editable.rb
+++ b/app/models/concerns/editable.rb
@@ -1,7 +1,7 @@
module Editable
extend ActiveSupport::Concern
- def is_edited?
+ def edited?
last_edited_at.present? && last_edited_at != created_at
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 32af5566135..3803e18a96e 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -8,6 +8,8 @@ module HasStatus
ACTIVE_STATUSES = %w[pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
ORDERED_STATUSES = %w[failed pending running manual canceled success skipped created].freeze
+ STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3,
+ failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
class_methods do
def status_sql
diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb
index 67a0adfcd56..a3d0ac8d862 100644
--- a/app/models/concerns/internal_id.rb
+++ b/app/models/concerns/internal_id.rb
@@ -9,7 +9,7 @@ module InternalId
def set_iid
if iid.blank?
parent = project || group
- records = parent.send(self.class.name.tableize)
+ records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend
records = records.with_deleted if self.paranoid?
max_iid = records.maximum(:iid)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 3731b7c8577..265f6e48540 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -6,6 +6,7 @@
#
module Issuable
extend ActiveSupport::Concern
+ include Gitlab::SQL::Pattern
include CacheMarkdownField
include Participable
include Mentionable
@@ -122,7 +123,9 @@ module Issuable
#
# Returns an ActiveRecord::Relation.
def search(query)
- where(arel_table[:title].matches("%#{query}%"))
+ title = to_fuzzy_arel(:title, query)
+
+ where(title)
end
# Searches for records with a matching title or description.
@@ -133,10 +136,10 @@ module Issuable
#
# Returns an ActiveRecord::Relation.
def full_search(query)
- t = arel_table
- pattern = "%#{query}%"
+ title = to_fuzzy_arel(:title, query)
+ description = to_fuzzy_arel(:description, query)
- where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
+ where(title&.or(description))
end
def sort(method, excluded_labels: [])
@@ -331,4 +334,11 @@ module Issuable
metrics = self.metrics || create_metrics
metrics.record!
end
+
+ ##
+ # Override in issuable specialization
+ #
+ def first_contribution?
+ false
+ end
end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index c034bf9cbc0..1db6b2d2fa2 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -56,7 +56,7 @@ module Mentionable
end
self.class.mentionable_attrs.each do |attr, options|
- text = __send__(attr)
+ text = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend
options = options.merge(
cache_key: [self, attr],
author: author,
@@ -100,7 +100,7 @@ module Mentionable
end
self.class.mentionable_attrs.any? do |attr, _|
- __send__(attr) =~ reference_pattern
+ __send__(attr) =~ reference_pattern # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index f0998465822..710fc1ed647 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -70,19 +70,19 @@ module Milestoneish
due_date && due_date.past?
end
- def is_group_milestone?
+ def group_milestone?
false
end
- def is_project_milestone?
+ def project_milestone?
false
end
- def is_legacy_group_milestone?
+ def legacy_group_milestone?
false
end
- def is_dashboard_milestone?
+ def dashboard_milestone?
false
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index c7bdc997eca..1c4ddabcad5 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -24,6 +24,10 @@ module Noteable
DiscussionNote::NOTEABLE_TYPES.include?(base_class_name)
end
+ def discussions_rendered_on_frontend?
+ false
+ end
+
def discussion_notes
notes
end
@@ -38,7 +42,7 @@ module Noteable
def grouped_diff_discussions(*args)
# Doesn't use `discussion_notes`, because this may include commit diff notes
- # besides MR diff notes, that we do no want to display on the MR Changes tab.
+ # besides MR diff notes, that we do not want to display on the MR Changes tab.
notes.inc_relations_for_view.grouped_diff_discussions(*args)
end
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index 4865c0a14b1..ce69fd34ac5 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -82,7 +82,7 @@ module Participable
if attr.respond_to?(:call)
source.instance_exec(current_user, ext, &attr)
else
- process << source.__send__(attr)
+ process << source.__send__(attr) # rubocop:disable GitlabSecurity/PublicSend
end
end
when Enumerable, ActiveRecord::Relation
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 60734bc6660..cb59b4da3d7 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility
build_project_feature unless project_feature
access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
- project_feature.send(:write_attribute, field, access_level)
+ project_feature.__send__(:write_attribute, field, access_level) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index ef95d6b0f98..454374121f3 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -23,7 +23,7 @@ module ProtectedRef
# If we don't `protected_branch` or `protected_tag` would be empty and
# `project` cannot be delegated to it, which in turn would cause validations
# to fail.
- has_many :"#{type}_access_levels", dependent: :destroy, inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
+ has_many :"#{type}_access_levels", inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index 10f4be72016..78ac4f324e7 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -25,6 +25,11 @@ module Referable
to_reference(from_project)
end
+ included do
+ alias_method :non_referable_inspect, :inspect
+ alias_method :inspect, :referable_inspect
+ end
+
def referable_inspect
if respond_to?(:id)
"#<#{self.class.name} id:#{id} #{to_reference(full: true)}>"
@@ -33,10 +38,6 @@ module Referable
end
end
- def inspect
- referable_inspect
- end
-
module ClassMethods
# The character that prefixes the actual reference identifier
#
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index 7cb9a28a284..e961c97e337 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -10,8 +10,12 @@ module RelativePositioning
after_save :save_positionable_neighbours
end
+ def project_ids
+ [project.id]
+ end
+
def max_relative_position
- self.class.in_projects(project.id).maximum(:relative_position)
+ self.class.in_projects(project_ids).maximum(:relative_position)
end
def prev_relative_position
@@ -19,7 +23,7 @@ module RelativePositioning
if self.relative_position
prev_pos = self.class
- .in_projects(project.id)
+ .in_projects(project_ids)
.where('relative_position < ?', self.relative_position)
.maximum(:relative_position)
end
@@ -32,7 +36,7 @@ module RelativePositioning
if self.relative_position
next_pos = self.class
- .in_projects(project.id)
+ .in_projects(project_ids)
.where('relative_position > ?', self.relative_position)
.minimum(:relative_position)
end
@@ -59,7 +63,7 @@ module RelativePositioning
pos_after = before.next_relative_position
if before.shift_after?
- issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_after)
+ issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_after)
issue_to_move.move_after
@positionable_neighbours = [issue_to_move]
@@ -74,7 +78,7 @@ module RelativePositioning
pos_before = after.prev_relative_position
if after.shift_before?
- issue_to_move = self.class.in_projects(project.id).find_by!(relative_position: pos_before)
+ issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_before)
issue_to_move.move_before
@positionable_neighbours = [issue_to_move]
diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb
index dd979e7bb17..f006a271327 100644
--- a/app/models/concerns/resolvable_discussion.rb
+++ b/app/models/concerns/resolvable_discussion.rb
@@ -24,6 +24,7 @@ module ResolvableDiscussion
delegate :resolved_at,
:resolved_by,
+ :resolved_by_push?,
to: :last_resolved_note,
allow_nil: true
diff --git a/app/models/concerns/resolvable_note.rb b/app/models/concerns/resolvable_note.rb
index 05eb6f86704..668c5a079e3 100644
--- a/app/models/concerns/resolvable_note.rb
+++ b/app/models/concerns/resolvable_note.rb
@@ -51,22 +51,34 @@ module ResolvableNote
end
# If you update this method remember to also update `.resolve!`
- def resolve!(current_user)
- return unless resolvable?
- return if resolved?
+ def resolve_without_save(current_user, resolved_by_push: false)
+ return false unless resolvable?
+ return false if resolved?
self.resolved_at = Time.now
self.resolved_by = current_user
- save!
+ self.resolved_by_push = resolved_by_push
+
+ true
end
# If you update this method remember to also update `.unresolve!`
- def unresolve!
- return unless resolvable?
- return unless resolved?
+ def unresolve_without_save
+ return false unless resolvable?
+ return false unless resolved?
self.resolved_at = nil
self.resolved_by = nil
- save!
+
+ true
+ end
+
+ def resolve!(current_user, resolved_by_push: false)
+ resolve_without_save(current_user, resolved_by_push: resolved_by_push) &&
+ save!
+ end
+
+ def unresolve!
+ unresolve_without_save && save!
end
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index a155a064032..db3cd257584 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -6,10 +6,6 @@ module Sortable
extend ActiveSupport::Concern
included do
- # By default all models should be ordered
- # by created_at field starting from newest
- default_scope { order_id_desc }
-
scope :order_id_desc, -> { reorder(id: :desc) }
scope :order_id_asc, -> { reorder(id: :asc) }
scope :order_created_desc, -> { reorder(created_at: :desc) }
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index bd75f25a210..731d9b9a745 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -28,7 +28,7 @@ module Spammable
def submittable_as_spam?
if user_agent_detail
- user_agent_detail.submittable? && current_application_settings.akismet_enabled
+ user_agent_detail.submittable? && Gitlab::CurrentSettings.current_application_settings.akismet_enabled
else
false
end
@@ -58,7 +58,7 @@ module Spammable
options.fetch(:spam_title, false)
end
- public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
+ public_send(attr.first) if attr && respond_to?(attr.first.to_sym) # rubocop:disable GitlabSecurity/PublicSend
end
def spam_description
@@ -66,12 +66,12 @@ module Spammable
options.fetch(:spam_description, false)
end
- public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
+ public_send(attr.first) if attr && respond_to?(attr.first.to_sym) # rubocop:disable GitlabSecurity/PublicSend
end
def spammable_text
result = self.class.spammable_attrs.map do |attr|
- public_send(attr.first)
+ public_send(attr.first) # rubocop:disable GitlabSecurity/PublicSend
end
result.reject(&:blank?).join("\n")
diff --git a/app/models/concerns/storage/legacy_project.rb b/app/models/concerns/storage/legacy_project.rb
deleted file mode 100644
index 815db712285..00000000000
--- a/app/models/concerns/storage/legacy_project.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-module Storage
- module LegacyProject
- extend ActiveSupport::Concern
-
- def disk_path
- full_path
- end
-
- def ensure_storage_path_exist
- gitlab_shell.add_namespace(repository_storage_path, namespace.full_path)
- end
-
- def rename_repo
- path_was = previous_changes['path'].first
- old_path_with_namespace = File.join(namespace.full_path, path_was)
- new_path_with_namespace = File.join(namespace.full_path, path)
-
- Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}"
-
- if has_container_registry_tags?
- Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present!"
-
- # we currently doesn't support renaming repository if it contains images in container registry
- raise StandardError.new('Project cannot be renamed, because images are present in its container registry')
- end
-
- expire_caches_before_rename(old_path_with_namespace)
-
- if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace)
- # If repository moved successfully we need to send update instructions to users.
- # However we cannot allow rollback since we moved repository
- # So we basically we mute exceptions in next actions
- begin
- gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
- send_move_instructions(old_path_with_namespace)
- expires_full_path_cache
-
- @old_path_with_namespace = old_path_with_namespace
-
- SystemHooksService.new.execute_hooks_for(self, :rename)
-
- @repository = nil
- rescue => e
- Rails.logger.error "Exception renaming #{old_path_with_namespace} -> #{new_path_with_namespace}: #{e}"
- # Returning false does not rollback after_* transaction but gives
- # us information about failing some of tasks
- false
- end
- else
- Rails.logger.error "Repository could not be renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"
-
- # if we cannot move namespace directory we should rollback
- # db changes in order to prevent out of sync between db and fs
- raise StandardError.new('repository cannot be renamed')
- end
-
- Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"
-
- Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.full_path)
- Gitlab::PagesTransfer.new.rename_project(path_was, path, namespace.full_path)
- end
-
- def create_repository(force: false)
- # Forked import is handled asynchronously
- return if forked? && !force
-
- if gitlab_shell.add_repository(repository_storage_path, path_with_namespace)
- repository.after_create
- true
- else
- errors.add(:base, 'Failed to create repository via gitlab-shell')
- false
- end
- end
- end
-end
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 1ca7f91dc03..a7d5de48c66 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -44,7 +44,8 @@ module TokenAuthenticatable
end
define_method("ensure_#{token_field}!") do
- send("reset_#{token_field}!") if read_attribute(token_field).blank?
+ send("reset_#{token_field}!") if read_attribute(token_field).blank? # rubocop:disable GitlabSecurity/PublicSend
+
read_attribute(token_field)
end
diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb
index fac7c5e5c85..86eb4ec76fc 100644
--- a/app/models/dashboard_milestone.rb
+++ b/app/models/dashboard_milestone.rb
@@ -3,7 +3,7 @@ class DashboardMilestone < GlobalMilestone
{ authorized_only: true }
end
- def is_dashboard_milestone?
+ def dashboard_milestone?
true
end
end
diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb
index ae8486bd9ac..b37b9bfbdac 100644
--- a/app/models/deploy_keys_project.rb
+++ b/app/models/deploy_keys_project.rb
@@ -12,7 +12,7 @@ class DeployKeysProject < ActiveRecord::Base
def destroy_orphaned_deploy_key
return unless self.deploy_key.destroyed_when_orphaned? && self.deploy_key.orphaned?
-
+
self.deploy_key.destroy
end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 056c49e7162..7bcded5b5e1 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -49,7 +49,7 @@ class Deployment < ActiveRecord::Base
# created before then could have a `sha` referring to a commit that no
# longer exists in the repository, so just ignore those.
begin
- project.repository.is_ancestor?(commit.id, sha)
+ project.repository.ancestor?(commit.id, sha)
rescue Rugged::OdbError
false
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index d1cec7613af..b80da7b246a 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -81,6 +81,10 @@ class Discussion
last_note.author
end
+ def updated?
+ last_updated_at != created_at
+ end
+
def id
first_note.discussion_id(context_noteable)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index e9ebf0637f3..435eeaf0e2e 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -114,7 +114,7 @@ class Environment < ActiveRecord::Base
end
def ref_path
- "refs/environments/#{Shellwords.shellescape(name)}"
+ "refs/#{Repository::REF_ENVIRONMENTS}/#{Shellwords.shellescape(name)}"
end
def formatted_external_url
diff --git a/app/models/event.rb b/app/models/event.rb
index 8d93a228494..c313bbb66f8 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -1,5 +1,6 @@
class Event < ActiveRecord::Base
include Sortable
+ include IgnorableColumn
default_scope { reorder(nil).where.not(author_id: nil) }
CREATED = 1
@@ -48,9 +49,7 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
-
- # For Hash only
- serialize :data # rubocop:disable Cop/ActiveRecordSerialize
+ has_one :push_event_payload, foreign_key: :event_id
# Callbacks
after_create :reset_project_activity
@@ -60,14 +59,53 @@ class Event < ActiveRecord::Base
scope :recent, -> { reorder(id: :desc) }
scope :code_push, -> { where(action: PUSHED) }
- scope :in_projects, ->(projects) do
- where(project_id: projects.pluck(:id)).recent
+ scope :in_projects, -> (projects) do
+ sub_query = projects
+ .except(:order)
+ .select(1)
+ .where('projects.id = events.project_id')
+
+ where('EXISTS (?)', sub_query).recent
+ end
+
+ scope :with_associations, -> do
+ # We're using preload for "push_event_payload" as otherwise the association
+ # is not always available (depending on the query being built).
+ includes(:author, :project, project: :namespace)
+ .preload(:target, :push_event_payload)
end
- scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
+ self.inheritance_column = 'action'
+
+ # "data" will be removed in 10.0 but it may be possible that JOINs happen that
+ # include this column, hence we're ignoring it as well.
+ ignore_column :data
+
class << self
+ def model_name
+ ActiveModel::Name.new(self, nil, 'event')
+ end
+
+ def find_sti_class(action)
+ if action.to_i == PUSHED
+ PushEvent
+ else
+ Event
+ end
+ end
+
+ def subclass_from_attributes(attrs)
+ # Without this Rails will keep calling this method on the returned class,
+ # resulting in an infinite loop.
+ return unless self == Event
+
+ action = attrs.with_indifferent_access[inheritance_column].to_i
+
+ PushEvent if action == PUSHED
+ end
+
# Update Gitlab::ContributionsCalendar#activity_dates if this changes
def contributions
where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
@@ -122,7 +160,7 @@ class Event < ActiveRecord::Base
end
def push?
- action == PUSHED && valid_push?
+ false
end
def merged?
@@ -235,77 +273,6 @@ class Event < ActiveRecord::Base
end
end
- def valid_push?
- data[:ref] && ref_name.present?
- rescue
- false
- end
-
- def tag?
- Gitlab::Git.tag_ref?(data[:ref])
- end
-
- def branch?
- Gitlab::Git.branch_ref?(data[:ref])
- end
-
- def new_ref?
- Gitlab::Git.blank_ref?(commit_from)
- end
-
- def rm_ref?
- Gitlab::Git.blank_ref?(commit_to)
- end
-
- def md_ref?
- !(rm_ref? || new_ref?)
- end
-
- def commit_from
- data[:before]
- end
-
- def commit_to
- data[:after]
- end
-
- def ref_name
- if tag?
- tag_name
- else
- branch_name
- end
- end
-
- def branch_name
- @branch_name ||= Gitlab::Git.ref_name(data[:ref])
- end
-
- def tag_name
- @tag_name ||= Gitlab::Git.ref_name(data[:ref])
- end
-
- # Max 20 commits from push DESC
- def commits
- @commits ||= (data[:commits] || []).reverse
- end
-
- def commits_count
- data[:total_commits_count] || commits.count || 0
- end
-
- def ref_type
- tag? ? "tag" : "branch"
- end
-
- def push_with_commits?
- !commits.empty? && commit_from && commit_to
- end
-
- def last_push_to_non_root?
- branch? && project.default_branch != branch_name
- end
-
def target_iid
target.respond_to?(:iid) ? target.iid : target_id
end
@@ -359,7 +326,7 @@ class Event < ActiveRecord::Base
def body?
if push?
- push_with_commits? || rm_ref?
+ push_with_commits?
elsif note?
true
else
@@ -385,6 +352,12 @@ class Event < ActiveRecord::Base
user ? author_id == user.id : false
end
+ def to_partial_path
+ # We are intentionally using `Event` rather than `self.class` so that
+ # subclasses also use the `Event` implementation.
+ Event._to_partial_path
+ end
+
private
def recent_update?
diff --git a/app/models/event_collection.rb b/app/models/event_collection.rb
new file mode 100644
index 00000000000..8b8244314af
--- /dev/null
+++ b/app/models/event_collection.rb
@@ -0,0 +1,98 @@
+# A collection of events to display in an event list.
+#
+# An EventCollection is meant to be used for displaying events to a user (e.g.
+# in a controller), it's not suitable for building queries that are used for
+# building other queries.
+class EventCollection
+ # To prevent users from putting too much pressure on the database by cycling
+ # through thousands of events we put a limit on the number of pages.
+ MAX_PAGE = 10
+
+ # projects - An ActiveRecord::Relation object that returns the projects for
+ # which to retrieve events.
+ # filter - An EventFilter instance to use for filtering events.
+ def initialize(projects, limit: 20, offset: 0, filter: nil)
+ @projects = projects
+ @limit = limit
+ @offset = offset
+ @filter = filter
+ end
+
+ # Returns an Array containing the events.
+ def to_a
+ return [] if current_page > MAX_PAGE
+
+ relation = if Gitlab::Database.join_lateral_supported?
+ relation_with_join_lateral
+ else
+ relation_without_join_lateral
+ end
+
+ relation.with_associations.to_a
+ end
+
+ private
+
+ # Returns the events relation to use when JOIN LATERAL is not supported.
+ #
+ # This relation simply gets all the events for all authorized projects, then
+ # limits that set.
+ def relation_without_join_lateral
+ events = filtered_events.in_projects(projects)
+
+ paginate_events(events)
+ end
+
+ # Returns the events relation to use when JOIN LATERAL is supported.
+ #
+ # This relation is built using JOIN LATERAL, producing faster queries than a
+ # regular LIMIT + OFFSET approach.
+ def relation_with_join_lateral
+ projects_for_lateral = projects.select(:id).to_sql
+
+ lateral = filtered_events
+ .limit(limit_for_join_lateral)
+ .where('events.project_id = projects_for_lateral.id')
+ .to_sql
+
+ # The outer query does not need to re-apply the filters since the JOIN
+ # LATERAL body already takes care of this.
+ outer = base_relation
+ .from("(#{projects_for_lateral}) projects_for_lateral")
+ .joins("JOIN LATERAL (#{lateral}) AS #{Event.table_name} ON true")
+
+ paginate_events(outer)
+ end
+
+ def filtered_events
+ @filter ? @filter.apply_filter(base_relation) : base_relation
+ end
+
+ def paginate_events(events)
+ events.limit(@limit).offset(@offset)
+ end
+
+ def base_relation
+ # We want to have absolute control over the event queries being built, thus
+ # we're explicitly opting out of any default scopes that may be set.
+ Event.unscoped.recent
+ end
+
+ def limit_for_join_lateral
+ # Applying the OFFSET on the inside of a JOIN LATERAL leads to incorrect
+ # results. To work around this we need to increase the inner limit for every
+ # page.
+ #
+ # This means that on page 1 we use LIMIT 20, and an outer OFFSET of 0. On
+ # page 2 we use LIMIT 40 and an outer OFFSET of 20.
+ @limit + @offset
+ end
+
+ def current_page
+ (@offset / @limit) + 1
+ end
+
+ def projects
+ @projects.except(:order)
+ end
+end
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
index 3df60ddc950..1633acd4fa9 100644
--- a/app/models/gpg_key.rb
+++ b/app/models/gpg_key.rb
@@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base
def verified_user_infos
user_infos.select do |user_info|
- user_info[:email] == user.email
+ user.verified_email?(user_info[:email])
end
end
@@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base
user_infos.map do |user_info|
[
user_info[:email],
- user_info[:email] == user.email
+ user.verified_email?(user_info[:email])
]
end.to_h
end
def verified?
- emails_with_verified_status.any? { |_email, verified| verified }
+ emails_with_verified_status.values.any?
+ end
+
+ def verified_and_belongs_to_email?(email)
+ emails_with_verified_status.fetch(email, false)
end
def update_invalid_gpg_signatures
@@ -78,11 +82,14 @@ class GpgKey < ActiveRecord::Base
end
def revoke
- GpgSignature.where(gpg_key: self, valid_signature: true).update_all(
- gpg_key_id: nil,
- valid_signature: false,
- updated_at: Time.zone.now
- )
+ GpgSignature
+ .where(gpg_key: self)
+ .where.not(verification_status: GpgSignature.verification_statuses[:unknown_key])
+ .update_all(
+ gpg_key_id: nil,
+ verification_status: GpgSignature.verification_statuses[:unknown_key],
+ updated_at: Time.zone.now
+ )
destroy
end
diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb
index 1ac0e123ff1..454c90d5fc4 100644
--- a/app/models/gpg_signature.rb
+++ b/app/models/gpg_signature.rb
@@ -1,9 +1,21 @@
class GpgSignature < ActiveRecord::Base
include ShaAttribute
+ include IgnorableColumn
+
+ ignore_column :valid_signature
sha_attribute :commit_sha
sha_attribute :gpg_key_primary_keyid
+ enum verification_status: {
+ unverified: 0,
+ verified: 1,
+ same_user_different_email: 2,
+ other_user: 3,
+ unverified_key: 4,
+ unknown_key: 5
+ }
+
belongs_to :project
belongs_to :gpg_key
@@ -18,4 +30,8 @@ class GpgSignature < ActiveRecord::Base
def commit
project.commit(commit_sha)
end
+
+ def gpg_commit
+ Gitlab::Gpg::Commit.new(commit)
+ end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index bd5735ed82e..e746e4a12c9 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -16,6 +16,7 @@ class Group < Namespace
source: :user
has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
+ has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
has_many :milestones
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -26,6 +27,8 @@ class Group < Namespace
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects
+ validate :visibility_level_allowed_by_sub_groups
+ validate :visibility_level_allowed_by_parent
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
@@ -102,15 +105,24 @@ class Group < Namespace
full_name
end
- def visibility_level_allowed_by_projects
- allowed_by_projects = self.projects.where('visibility_level > ?', self.visibility_level).none?
+ def visibility_level_allowed_by_parent?(level = self.visibility_level)
+ return true unless parent_id && parent_id.nonzero?
- unless allowed_by_projects
- level_name = Gitlab::VisibilityLevel.level_name(visibility_level).downcase
- self.errors.add(:visibility_level, "#{level_name} is not allowed since there are projects with higher visibility.")
- end
+ level <= parent.visibility_level
+ end
+
+ def visibility_level_allowed_by_projects?(level = self.visibility_level)
+ !projects.where('visibility_level > ?', level).exists?
+ end
+
+ def visibility_level_allowed_by_sub_groups?(level = self.visibility_level)
+ !children.where('visibility_level > ?', level).exists?
+ end
- allowed_by_projects
+ def visibility_level_allowed?(level = self.visibility_level)
+ visibility_level_allowed_by_parent?(level) &&
+ visibility_level_allowed_by_projects?(level) &&
+ visibility_level_allowed_by_sub_groups?(level)
end
def avatar_url(**args)
@@ -206,27 +218,45 @@ class Group < Namespace
SystemHooksService.new
end
- def refresh_members_authorized_projects
+ def refresh_members_authorized_projects(blocking: true)
UserProjectAccessChangedService.new(user_ids_for_project_authorizations)
- .execute
+ .execute(blocking: blocking)
end
def user_ids_for_project_authorizations
- users_with_parents.pluck(:id)
+ members_with_parents.pluck(:user_id)
end
def members_with_parents
- GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil)
+ # Avoids an unnecessary SELECT when the group has no parents
+ source_ids =
+ if parent_id
+ self_and_ancestors.reorder(nil).select(:id)
+ else
+ id
+ end
+
+ GroupMember
+ .active_without_invites
+ .where(source_id: source_ids)
+ end
+
+ def members_with_descendants
+ GroupMember
+ .active_without_invites
+ .where(source_id: self_and_descendants.reorder(nil).select(:id))
end
def users_with_parents
- User.where(id: members_with_parents.select(:user_id))
+ User
+ .where(id: members_with_parents.select(:user_id))
+ .reorder(nil)
end
def users_with_descendants
- members_with_descendants = GroupMember.non_request.where(source_id: descendants.pluck(:id).push(id))
-
- User.where(id: members_with_descendants.select(:user_id))
+ User
+ .where(id: members_with_descendants.select(:user_id))
+ .reorder(nil)
end
def max_member_access_for_user(user)
@@ -257,11 +287,29 @@ class Group < Namespace
list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten
end
- protected
+ private
def update_two_factor_requirement
return unless require_two_factor_authentication_changed? || two_factor_grace_period_changed?
users.find_each(&:update_two_factor_requirement)
end
+
+ def visibility_level_allowed_by_parent
+ return if visibility_level_allowed_by_parent?
+
+ errors.add(:visibility_level, "#{visibility} is not allowed since the parent group has a #{parent.visibility} visibility.")
+ end
+
+ def visibility_level_allowed_by_projects
+ return if visibility_level_allowed_by_projects?
+
+ errors.add(:visibility_level, "#{visibility} is not allowed since this group contains projects with higher visibility.")
+ end
+
+ def visibility_level_allowed_by_sub_groups
+ return if visibility_level_allowed_by_sub_groups?
+
+ errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.")
+ end
end
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
index 65249bd7bfc..98135ee3c8b 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/group_milestone.rb
@@ -17,7 +17,7 @@ class GroupMilestone < GlobalMilestone
{ group_id: group.id }
end
- def is_legacy_group_milestone?
+ def legacy_group_milestone?
true
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 1c948c8957e..8c7d492e605 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -9,11 +9,8 @@ class Issue < ActiveRecord::Base
include Spammable
include FasterCacheKeys
include RelativePositioning
- include IgnorableColumn
include CreatedAtFilterable
- ignore_column :position
-
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze
@@ -53,7 +50,10 @@ class Issue < ActiveRecord::Base
scope :preload_associations, -> { preload(:labels, project: :namespace) }
+ scope :public_only, -> { where(confidential: false) }
+
after_save :expire_etag_cache
+ after_commit :update_project_counter_caches, on: :destroy
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
@@ -269,6 +269,20 @@ class Issue < ActiveRecord::Base
end
end
+ def discussions_rendered_on_frontend?
+ true
+ end
+
+ def update_project_counter_caches?
+ state_changed? || confidential_changed?
+ end
+
+ def update_project_counter_caches
+ return unless update_project_counter_caches?
+
+ Projects::OpenIssuesCountService.new(project).refresh_cache
+ end
+
private
# Returns `true` if the given User can read the current Issue.
diff --git a/app/models/key.rb b/app/models/key.rb
index 49bc26122fa..a6b4dcfec0d 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -1,6 +1,7 @@
require 'digest/md5'
class Key < ActiveRecord::Base
+ include Gitlab::CurrentSettings
include Sortable
LAST_USED_AT_REFRESH_TIME = 1.day.to_i
@@ -12,14 +13,18 @@ class Key < ActiveRecord::Base
validates :title,
presence: true,
length: { maximum: 255 }
+
validates :key,
presence: true,
length: { maximum: 5000 },
format: { with: /\A(ssh|ecdsa)-.*\Z/ }
+
validates :fingerprint,
uniqueness: true,
presence: { message: 'cannot be generated' }
+ validate :key_meets_restrictions
+
delegate :name, :email, to: :user, prefix: true
after_commit :add_to_shell, on: :create
@@ -80,6 +85,10 @@ class Key < ActiveRecord::Base
SystemHooksService.new.execute_hooks_for(self, :destroy)
end
+ def public_key
+ @public_key ||= Gitlab::SSHPublicKey.new(key)
+ end
+
private
def generate_fingerprint
@@ -87,7 +96,27 @@ class Key < ActiveRecord::Base
return unless self.key.present?
- self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint
+ self.fingerprint = public_key.fingerprint
+ end
+
+ def key_meets_restrictions
+ restriction = current_application_settings.key_restriction_for(public_key.type)
+
+ if restriction == ApplicationSetting::FORBIDDEN_KEY_VALUE
+ errors.add(:key, forbidden_key_type_message)
+ elsif public_key.bits < restriction
+ errors.add(:key, "must be at least #{restriction} bits")
+ end
+ end
+
+ def forbidden_key_type_message
+ allowed_types =
+ current_application_settings
+ .allowed_key_types
+ .map(&:upcase)
+ .to_sentence(last_word_connector: ', or ', two_words_connector: ' or ')
+
+ "type is forbidden. Must be #{allowed_types}"
end
def notify_user
diff --git a/app/models/label.rb b/app/models/label.rb
index 674bb3f2720..958141a7358 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -34,7 +34,8 @@ class Label < ActiveRecord::Base
scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) }
- scope :on_project_boards, ->(project_id) { joins(lists: :board).merge(List.movable).where(boards: { project_id: project_id }) }
+ scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) }
+ scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
def self.prioritized(project)
joins(:priorities)
@@ -172,6 +173,7 @@ class Label < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
+ json[:type] = self.try(:type)
json[:priority] = priority(options[:project]) if options.key?(:project)
end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index dc9247bc9a0..cbbd58f2eaf 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -41,9 +41,20 @@ class Member < ActiveRecord::Base
is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil))
user_is_active = User.arel_table[:state].eq(:active)
- includes(:user).references(:users)
- .where(is_external_invite.or(user_is_active))
+ user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active)
+
+ left_join_users
+ .where(user_ok)
+ .where(requested_at: nil)
+ .reorder(nil)
+ end
+
+ # Like active, but without invites. For when a User is required.
+ scope :active_without_invites, -> do
+ left_join_users
+ .where(users: { state: 'active' })
.where(requested_at: nil)
+ .reorder(nil)
end
scope :invite, -> { where.not(invite_token: nil) }
@@ -115,20 +126,11 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token)
end
- def add_user(source, user, access_level, current_user: nil, expires_at: nil)
- user = retrieve_user(user)
+ def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil)
+ # `user` can be either a User object, User ID or an email to be invited
+ member = retrieve_member(source, user, existing_members)
access_level = retrieve_access_level(access_level)
- # `user` can be either a User object or an email to be invited
- member =
- if user.is_a?(User)
- source.members.find_by(user_id: user.id) ||
- source.requesters.find_by(user_id: user.id) ||
- source.members.build(user_id: user.id)
- else
- source.members.build(invite_email: user)
- end
-
return member unless can_update_member?(current_user, member)
member.attributes = {
@@ -154,17 +156,15 @@ class Member < ActiveRecord::Base
def add_users(source, users, access_level, current_user: nil, expires_at: nil)
return [] unless users.present?
- # Collect all user ids into separate array
- # so we can use single sql query to get user objects
- user_ids = users.select { |user| user =~ /\A\d+\Z/ }
- users = users - user_ids + User.where(id: user_ids)
+ emails, users, existing_members = parse_users_list(source, users)
self.transaction do
- users.map do |user|
+ (emails + users).map! do |user|
add_user(
source,
user,
access_level,
+ existing_members: existing_members,
current_user: current_user,
expires_at: expires_at
)
@@ -178,6 +178,31 @@ class Member < ActiveRecord::Base
private
+ def parse_users_list(source, list)
+ emails, user_ids, users = [], [], []
+ existing_members = {}
+
+ list.each do |item|
+ case item
+ when User
+ users << item
+ when Integer
+ user_ids << item
+ when /\A\d+\Z/
+ user_ids << item.to_i
+ when Devise.email_regexp
+ emails << item
+ end
+ end
+
+ if user_ids.present?
+ users.concat(User.where(id: user_ids))
+ existing_members = source.members_and_requesters.where(user_id: user_ids).index_by(&:user_id)
+ end
+
+ [emails, users, existing_members]
+ end
+
# This method is used to find users that have been entered into the "Add members" field.
# These can be the User objects directly, their IDs, their emails, or new emails to be invited.
def retrieve_user(user)
@@ -186,6 +211,20 @@ class Member < ActiveRecord::Base
User.find_by(id: user) || User.find_by(email: user) || user
end
+ def retrieve_member(source, user, existing_members)
+ user = retrieve_user(user)
+
+ if user.is_a?(User)
+ if existing_members
+ existing_members[user.id] || source.members.build(user_id: user.id)
+ else
+ source.members_and_requesters.find_or_initialize_by(user_id: user.id)
+ end
+ else
+ source.members.build(invite_email: user)
+ end
+ end
+
def retrieve_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i }
end
@@ -276,6 +315,13 @@ class Member < ActiveRecord::Base
@notification_setting ||= user.notification_settings_for(source)
end
+ def notifiable?(type, opts = {})
+ # always notify when there isn't a user yet
+ return true if user.blank?
+
+ NotificationRecipientService.notifiable?(user, type, notifiable_options.merge(opts))
+ end
+
private
def send_invite
@@ -332,4 +378,8 @@ class Member < ActiveRecord::Base
def notification_service
NotificationService.new
end
+
+ def notifiable_options
+ {}
+ end
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 47040f95533..661e668dbf9 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -30,6 +30,10 @@ class GroupMember < Member
'Group'
end
+ def notifiable_options
+ { group: group }
+ end
+
private
def send_invite
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index c0e17f4bfc8..b6f1dd272cd 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -87,6 +87,10 @@ class ProjectMember < Member
project.owner == user
end
+ def notifiable_options
+ { project: project }
+ end
+
private
def delete_member_todos
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index e83b11f7668..2a56bab48a3 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -7,7 +7,6 @@ class MergeRequest < ActiveRecord::Base
include IgnorableColumn
include CreatedAtFilterable
- ignore_column :position
ignore_column :locked_at
belongs_to :target_project, class_name: "Project"
@@ -32,6 +31,7 @@ class MergeRequest < ActiveRecord::Base
after_create :ensure_merge_request_diff, unless: :importing?
after_update :reload_diff_if_branch_changed
+ after_commit :update_project_counter_caches, on: :destroy
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -162,7 +162,7 @@ class MergeRequest < ActiveRecord::Base
target = unscoped.where(target_project_id: relation).select(:id)
union = Gitlab::SQL::Union.new([source, target])
- where("merge_requests.id IN (#{union.to_sql})")
+ where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
@@ -241,6 +241,14 @@ class MergeRequest < ActiveRecord::Base
end
end
+ # Calls `MergeWorker` to proceed with the merge process and
+ # updates `merge_jid` with the MergeWorker#jid.
+ # This helps tracking enqueued and ongoing merge jobs.
+ def merge_async(user_id, params)
+ jid = MergeWorker.perform_async(id, user_id, params)
+ update_column(:merge_jid, jid)
+ end
+
def first_commit
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end
@@ -384,9 +392,7 @@ class MergeRequest < ActiveRecord::Base
end
def merge_ongoing?
- return false unless merge_jid
-
- Gitlab::SidekiqStatus.num_running([merge_jid]) > 0
+ !!merge_jid && !merged?
end
def closed_without_fork?
@@ -443,7 +449,8 @@ class MergeRequest < ActiveRecord::Base
end
def reload_diff_if_branch_changed
- if source_branch_changed? || target_branch_changed?
+ if (source_branch_changed? || target_branch_changed?) &&
+ (source_branch_head && target_branch_head)
reload_diff
end
end
@@ -598,6 +605,8 @@ class MergeRequest < ActiveRecord::Base
self.merge_requests_closing_issues.delete_all
closes_issues(current_user).each do |issue|
+ next if issue.is_a?(ExternalIssue)
+
self.merge_requests_closing_issues.create!(issue: issue)
end
end
@@ -682,9 +691,8 @@ class MergeRequest < ActiveRecord::Base
if !include_description && closes_issues_references.present?
message << "Closes #{closes_issues_references.to_sentence}"
end
-
message << "#{description}" if include_description && description.present?
- message << "See merge request #{to_reference}"
+ message << "See merge request #{to_reference(full: true)}"
message.join("\n\n")
end
@@ -792,16 +800,12 @@ class MergeRequest < ActiveRecord::Base
end
def fetch_ref
- target_project.repository.fetch_ref(
- source_project.repository.path_to_repo,
- "refs/heads/#{source_branch}",
- ref_path
- )
+ write_ref
update_column(:ref_fetched, true)
end
def ref_path
- "refs/merge-requests/#{iid}/head"
+ "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end
def ref_fetched?
@@ -823,7 +827,7 @@ class MergeRequest < ActiveRecord::Base
lock_mr
yield
ensure
- unlock_mr if locked?
+ unlock_mr
end
end
@@ -914,6 +918,12 @@ class MergeRequest < ActiveRecord::Base
active_diff_discussions.each do |discussion|
service.execute(discussion)
end
+
+ if project.resolve_outdated_diff_discussions?
+ MergeRequests::ResolvedDiscussionNotificationService
+ .new(project, current_user)
+ .execute(self)
+ end
end
def keep_around_commit
@@ -939,4 +949,26 @@ class MergeRequest < ActiveRecord::Base
true
end
+
+ def update_project_counter_caches?
+ state_changed?
+ end
+
+ def update_project_counter_caches
+ return unless update_project_counter_caches?
+
+ Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
+ end
+
+ def first_contribution?
+ return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST
+
+ project.merge_requests.merged.where(author_id: author_id).empty?
+ end
+
+ private
+
+ def write_ref
+ target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path)
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index d9d746ccf41..58050e1f438 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -282,9 +282,7 @@ class MergeRequestDiff < ActiveRecord::Base
def load_commits
commits = st_commits.presence || merge_request_diff_commits
- commits.map do |commit|
- Commit.new(Gitlab::Git::Commit.new(commit.to_hash), merge_request.source_project)
- end
+ commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
end
def save_diffs
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index cafdbe11849..670b26d4ca3 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -26,7 +26,7 @@ class MergeRequestDiffCommit < ActiveRecord::Base
def to_hash
Gitlab::Git::Commit::SERIALIZE_KEYS.each_with_object({}) do |key, hash|
- hash[key] = public_send(key)
+ hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 01e0d0155a3..a3070a12b7c 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -163,7 +163,7 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
def to_reference(from_project = nil, format: :iid, full: false)
- return if is_group_milestone? && format != :name
+ return if group_milestone? && format != :name
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
@@ -207,11 +207,11 @@ class Milestone < ActiveRecord::Base
group || project
end
- def is_group_milestone?
+ def group_milestone?
group_id.present?
end
- def is_project_milestone?
+ def project_milestone?
project_id.present?
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 6073fb94a3f..4a9a23fea1f 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -44,6 +44,10 @@ class Namespace < ActiveRecord::Base
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
+ before_create :sync_share_with_group_lock_with_parent
+ before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
+ after_update :force_share_with_group_lock_on_descendants, if: -> { share_with_group_lock_changed? && share_with_group_lock? }
+
# Legacy Storage specific hooks
after_update :move_dir, if: :path_changed?
@@ -156,6 +160,14 @@ class Namespace < ActiveRecord::Base
.base_and_ancestors
end
+ def self_and_ancestors
+ return self.class.where(id: id) unless parent_id
+
+ Gitlab::GroupHierarchy
+ .new(self.class.where(id: id))
+ .base_and_ancestors
+ end
+
# Returns all the descendants of the current namespace.
def descendants
Gitlab::GroupHierarchy
@@ -163,6 +175,12 @@ class Namespace < ActiveRecord::Base
.base_and_descendants
end
+ def self_and_descendants
+ Gitlab::GroupHierarchy
+ .new(self.class.where(id: id))
+ .base_and_descendants
+ end
+
def user_ids_for_project_authorizations
[owner_id]
end
@@ -181,6 +199,10 @@ class Namespace < ActiveRecord::Base
parent.present?
end
+ def subgroup?
+ has_parent?
+ end
+
def soft_delete_without_removing_associations
# We can't use paranoia's `#destroy` since this will hard-delete projects.
# Project uses `pending_delete` instead of the acts_as_paranoia gem.
@@ -201,4 +223,14 @@ class Namespace < ActiveRecord::Base
errors.add(:parent_id, "has too deep level of nesting")
end
end
+
+ def sync_share_with_group_lock_with_parent
+ if parent&.share_with_group_lock?
+ self.share_with_group_lock = true
+ end
+ end
+
+ def force_share_with_group_lock_on_descendants
+ descendants.update_all(share_with_group_lock: true)
+ end
end
diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb
index 8417f200e36..9357e55b419 100644
--- a/app/models/network/commit.rb
+++ b/app/models/network/commit.rb
@@ -12,7 +12,7 @@ module Network
end
def method_missing(m, *args, &block)
- @commit.send(m, *args, &block)
+ @commit.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def space
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 2bc00a082df..3845e485413 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -152,14 +152,14 @@ module Network
end
def find_free_parent_space(range, space_base, space_step, space_default)
- if is_overlap?(range, space_default)
+ if overlap?(range, space_default)
find_free_space(range, space_step, space_base, space_default)
else
space_default
end
end
- def is_overlap?(range, overlap_space)
+ def overlap?(range, overlap_space)
range.each do |i|
if i != range.first &&
i != range.last &&
@@ -206,7 +206,7 @@ module Network
# Visit branching chains
leaves.each do |l|
- parents = l.parents(@map).select{|p| p.space.zero?}
+ parents = l.parents(@map).select {|p| p.space.zero?}
parents.each do |p|
place_chain(p, l.time)
end
diff --git a/app/models/note.rb b/app/models/note.rb
index d0e3bc0bfed..f44590e2144 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -15,6 +15,16 @@ class Note < ActiveRecord::Base
include IgnorableColumn
include Editable
+ module SpecialRole
+ FIRST_TIME_CONTRIBUTOR = :first_time_contributor
+
+ class << self
+ def values
+ constants.map {|const| self.const_get(const)}
+ end
+ end
+ end
+
ignore_column :original_discussion_id
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
@@ -32,9 +42,12 @@ class Note < ActiveRecord::Base
# Banzai::ObjectRenderer
attr_accessor :user_visible_reference_count
- # Attribute used to store the attributes that have ben changed by quick actions.
+ # Attribute used to store the attributes that have been changed by quick actions.
attr_accessor :commands_changes
+ # A special role that may be displayed on issuable's discussions
+ attr_accessor :special_role
+
default_value_for :system, false
attr_mentionable :note, pipeline: :note
@@ -77,20 +90,20 @@ class Note < ActiveRecord::Base
# Scopes
scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) }
- scope :system, ->{ where(system: true) }
- scope :user, ->{ where(system: false) }
- scope :common, ->{ where(noteable_type: ["", nil]) }
- scope :fresh, ->{ order(created_at: :asc, id: :asc) }
- scope :updated_after, ->(time){ where('updated_at > ?', time) }
- scope :inc_author_project, ->{ includes(:project, :author) }
- scope :inc_author, ->{ includes(:author) }
+ scope :system, -> { where(system: true) }
+ scope :user, -> { where(system: false) }
+ scope :common, -> { where(noteable_type: ["", nil]) }
+ scope :fresh, -> { order(created_at: :asc, id: :asc) }
+ scope :updated_after, ->(time) { where('updated_at > ?', time) }
+ scope :inc_author_project, -> { includes(:project, :author) }
+ scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> do
includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata)
end
- scope :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) }
- scope :new_diff_notes, ->{ where(type: 'DiffNote') }
- scope :non_diff_notes, ->{ where(type: ['Note', 'DiscussionNote', nil]) }
+ scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) }
+ scope :new_diff_notes, -> { where(type: 'DiffNote') }
+ scope :non_diff_notes, -> { where(type: ['Note', 'DiscussionNote', nil]) }
scope :with_associations, -> do
# FYI noteable cannot be loaded for LegacyDiffNote for commits
@@ -141,6 +154,10 @@ class Note < ActiveRecord::Base
.group(:noteable_id)
.where(noteable_type: type, noteable_id: ids)
end
+
+ def has_special_role?(role, note)
+ note.special_role == role
+ end
end
def cross_reference?
@@ -206,6 +223,22 @@ class Note < ActiveRecord::Base
super(noteable_type.to_s.classify.constantize.base_class.to_s)
end
+ def special_role=(role)
+ raise "Role is undefined, #{role} not found in #{SpecialRole.values}" unless SpecialRole.values.include?(role)
+
+ @special_role = role
+ end
+
+ def has_special_role?(role)
+ self.class.has_special_role?(role, self)
+ end
+
+ def specialize_for_first_contribution!(noteable)
+ return unless noteable.author_id == self.author_id
+
+ self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
+ end
+
def editable?
!system?
end
@@ -299,6 +332,17 @@ class Note < ActiveRecord::Base
end
end
+ def expire_etag_cache
+ return unless noteable&.discussions_rendered_on_frontend?
+
+ key = Gitlab::Routing.url_helpers.project_noteable_notes_path(
+ project,
+ target_type: noteable_type.underscore,
+ target_id: noteable_id
+ )
+ Gitlab::EtagCaching::Store.new.touch(key)
+ end
+
private
def keep_around_commit
@@ -326,15 +370,4 @@ class Note < ActiveRecord::Base
def set_discussion_id
self.discussion_id ||= discussion_class.discussion_id(self)
end
-
- def expire_etag_cache
- return unless for_issue?
-
- key = Gitlab::Routing.url_helpers.project_noteable_notes_path(
- noteable.project,
- target_type: noteable_type.underscore,
- target_id: noteable.id
- )
- Gitlab::EtagCaching::Store.new.touch(key)
- end
end
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 418b42d8f1d..183e098d819 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -5,60 +5,67 @@ class NotificationRecipient
custom_action: nil,
target: nil,
acting_user: nil,
- project: nil
+ project: nil,
+ group: nil,
+ skip_read_ability: false
)
+ unless NotificationSetting.levels.key?(type) || type == :subscription
+ raise ArgumentError, "invalid type: #{type.inspect}"
+ end
+
@custom_action = custom_action
@acting_user = acting_user
@target = target
- @project = project || @target&.project
+ @project = project || default_project
+ @group = group || @project&.group
@user = user
@type = type
+ @skip_read_ability = skip_read_ability
end
def notification_setting
@notification_setting ||= find_notification_setting
end
- def raw_notification_level
- notification_setting&.level&.to_sym
- end
-
def notification_level
- # custom is treated the same as watch if it's enabled - otherwise it's
- # set to :custom, meaning to send exactly when our type is :participating
- # or :mention.
- @notification_level ||=
- case raw_notification_level
- when :custom
- if @custom_action && notification_setting&.event_enabled?(@custom_action)
- :watch
- else
- :custom
- end
- else
- raw_notification_level
- end
+ @notification_level ||= notification_setting&.level&.to_sym
end
def notifiable?
return false unless has_access?
return false if own_activity?
- return true if @type == :subscription
-
- return false if notification_level.nil? || notification_level == :disabled
-
- return %i[participating mention].include?(@type) if notification_level == :custom
-
- return false if %i[watch participating].include?(notification_level) && excluded_watcher_action?
+ # even users with :disabled notifications receive manual subscriptions
+ return !unsubscribed? if @type == :subscription
- return false unless NotificationSetting.levels[notification_level] <= NotificationSetting.levels[@type]
+ return false unless suitable_notification_level?
+ # check this last because it's expensive
+ # nobody should receive notifications if they've specifically unsubscribed
return false if unsubscribed?
true
end
+ def suitable_notification_level?
+ case notification_level
+ when :disabled, nil
+ false
+ when :custom
+ custom_enabled? || %i[participating mention].include?(@type)
+ when :watch, :participating
+ !excluded_watcher_action?
+ when :mention
+ @type == :mention
+ else
+ false
+ end
+ end
+
+ def custom_enabled?
+ @custom_action && notification_setting&.event_enabled?(@custom_action)
+ end
+
def unsubscribed?
return false unless @target
return false unless @target.respond_to?(:subscriptions)
@@ -77,6 +84,8 @@ class NotificationRecipient
def has_access?
DeclarativePolicy.subject_scope do
return false unless user.can?(:receive_notifications)
+ return true if @skip_read_ability
+
return false if @project && !user.can?(:read_project, @project)
return true unless read_ability
@@ -88,7 +97,7 @@ class NotificationRecipient
def excluded_watcher_action?
return false unless @custom_action
- return false if raw_notification_level == :custom
+ return false if notification_level == :custom
NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(@custom_action)
end
@@ -96,6 +105,7 @@ class NotificationRecipient
private
def read_ability
+ return nil if @skip_read_ability
return @read_ability if instance_variable_defined?(:@read_ability)
@read_ability =
@@ -111,12 +121,18 @@ class NotificationRecipient
end
end
+ def default_project
+ return nil if @target.nil?
+ return @target if @target.is_a?(Project)
+ return @target.project if @target.respond_to?(:project)
+ end
+
def find_notification_setting
project_setting = @project && user.notification_settings_for(@project)
return project_setting unless project_setting.nil? || project_setting.global?
- group_setting = @project&.group && user.notification_settings_for(@project.group)
+ group_setting = @group && user.notification_settings_for(@group)
return group_setting unless group_setting.nil? || group_setting.global?
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 9b1cac64c44..245f8dddcf9 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -66,6 +66,6 @@ class NotificationSetting < ActiveRecord::Base
alias_method :failed_pipeline?, :failed_pipeline
def event_enabled?(event)
- respond_to?(event) && !!public_send(event)
+ respond_to?(event) && !!public_send(event) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index e7baba2ef08..ff5638dd155 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -17,14 +17,15 @@ class Project < ActiveRecord::Base
include ProjectFeaturesCompatibility
include SelectForProjectAuthorization
include Routable
- include Storage::LegacyProject
extend Gitlab::ConfigHelper
+ extend Gitlab::CurrentSettings
BoardLimitExceeded = Class.new(StandardError)
NUMBER_OF_PERMITTED_BOARDS = 1
UNKNOWN_IMPORT_URL = 'http://unknown.git'.freeze
+ LATEST_STORAGE_VERSION = 1
cache_markdown_field :description, pipeline: :description
@@ -32,8 +33,11 @@ class Project < ActiveRecord::Base
:merge_requests_enabled?, :issues_enabled?, to: :project_feature,
allow_nil: true
+ delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage
+
default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level
+ default_value_for :resolve_outdated_diff_discussions, false
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) { current_application_settings.pick_repository_storage }
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
@@ -44,36 +48,27 @@ class Project < ActiveRecord::Base
default_value_for :snippets_enabled, gitlab_config_features.snippets
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
- after_create :ensure_storage_path_exist
- after_create :create_project_feature, unless: :project_feature
- after_save :update_project_statistics, if: :namespace_id_changed?
+ add_authentication_token_field :runners_token
+ before_save :ensure_runners_token
- # set last_activity_at to the same as created_at
+ after_save :update_project_statistics, if: :namespace_id_changed?
+ after_create :create_project_feature, unless: :project_feature
after_create :set_last_activity_at
- def set_last_activity_at
- update_column(:last_activity_at, self.created_at)
- end
-
after_create :set_last_repository_updated_at
- def set_last_repository_updated_at
- update_column(:last_repository_updated_at, self.created_at)
- end
+ after_update :update_forks_visibility_level
before_destroy :remove_private_deploy_keys
- after_destroy :remove_pages
-
- # update visibility_level of forks
- after_update :update_forks_visibility_level
+ after_destroy -> { run_after_commit { remove_pages } }
after_validation :check_pending_delete
- # Legacy Storage specific hooks
-
- after_save :ensure_storage_path_exist, if: :namespace_id_changed?
+ # Storage specific hooks
+ after_initialize :use_hashed_storage
+ after_create :ensure_storage_path_exists
+ after_save :ensure_storage_path_exists, if: :namespace_id_changed?
acts_as_taggable
- attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
attr_accessor :template_name
attr_writer :pipeline_status
@@ -150,6 +145,7 @@ class Project < ActiveRecord::Base
has_many :requesters, -> { where.not(requested_at: nil) },
as: :source, class_name: 'ProjectMember', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+ has_many :members_and_requesters, as: :source, class_name: 'ProjectMember'
has_many :deploy_keys_projects
has_many :deploy_keys, through: :deploy_keys_projects
@@ -191,12 +187,14 @@ class Project < ActiveRecord::Base
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
+ 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 :import_data
+ accepts_nested_attributes_for :auto_devops
delegate :name, to: :owner, allow_nil: true, prefix: true
- delegate :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
@@ -229,6 +227,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 :avatar_type,
if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
@@ -239,9 +238,6 @@ class Project < ActiveRecord::Base
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
- add_authentication_token_field :runners_token
- before_save :ensure_runners_token
-
mount_uploader :avatar, AvatarUploader
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -257,6 +253,7 @@ class Project < ActiveRecord::Base
scope :joined, ->(user) { where('namespace_id != ?', user.namespace_id) }
scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) }
scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) }
+ scope :archived, -> { where(archived: true) }
scope :non_archived, -> { where(archived: false) }
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
scope :with_push, -> { joins(:events).where('events.action = ?', Event::PUSHED) }
@@ -370,7 +367,10 @@ class Project < ActiveRecord::Base
state :failed
after_transition [:none, :finished, :failed] => :scheduled do |project, _|
- project.run_after_commit { add_import_job }
+ project.run_after_commit do
+ job_id = add_import_job
+ update(import_jid: job_id) if job_id
+ end
end
after_transition started: :finished do |project, _|
@@ -378,11 +378,7 @@ class Project < ActiveRecord::Base
if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists?
project.run_after_commit do
- begin
- Projects::HousekeepingService.new(project).execute
- rescue Projects::HousekeepingService::LeaseTaken => e
- Rails.logger.info("Could not perform housekeeping for project #{project.full_path} (#{project.id}): #{e}")
- end
+ Projects::AfterImportService.new(project).execute
end
end
end
@@ -415,7 +411,7 @@ class Project < ActiveRecord::Base
union = Gitlab::SQL::Union.new([projects, namespaces])
- where("projects.id IN (#{union.to_sql})")
+ where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
def search_by_title(query)
@@ -473,8 +469,20 @@ class Project < ActiveRecord::Base
self[:lfs_enabled] && Gitlab.config.lfs.enabled
end
+ def auto_devops_enabled?
+ if auto_devops&.enabled.nil?
+ current_application_settings.auto_devops_enabled?
+ else
+ auto_devops.enabled?
+ end
+ end
+
+ def has_auto_devops_implicitly_disabled?
+ auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled?
+ end
+
def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage]['path']
+ Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
end
def team
@@ -485,6 +493,10 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(full_path, self, disk_path: disk_path)
end
+ def reload_repository!
+ @repository = nil
+ end
+
def container_registry_url
if Gitlab.config.registry.enabled
"#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
@@ -525,17 +537,26 @@ class Project < ActiveRecord::Base
def add_import_job
job_id =
if forked?
- RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
- forked_from_project.full_path,
- self.namespace.full_path)
+ RepositoryForkWorker.perform_async(id,
+ forked_from_project.repository_storage_path,
+ forked_from_project.full_path,
+ self.namespace.full_path)
else
RepositoryImportWorker.perform_async(self.id)
end
+ log_import_activity(job_id)
+
+ job_id
+ end
+
+ def log_import_activity(job_id, type: :import)
+ job_type = type.to_s.capitalize
+
if job_id
- Rails.logger.info "Import job started for #{full_path} with job ID #{job_id}"
+ Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
else
- Rails.logger.error "Import job failed to start for #{full_path}"
+ Rails.logger.error("#{job_type} job failed to create for #{full_path}.")
end
end
@@ -544,6 +565,7 @@ class Project < ActiveRecord::Base
ProjectCacheWorker.perform_async(self.id)
end
+ update(import_error: nil)
remove_import_data
end
@@ -575,7 +597,7 @@ class Project < ActiveRecord::Base
end
def valid_import_url?
- valid? || errors.messages[:import_url].nil?
+ valid?(:import_url) || errors.messages[:import_url].nil?
end
def create_or_update_import_data(data: nil, credentials: nil)
@@ -825,7 +847,7 @@ class Project < ActiveRecord::Base
if template.nil?
# If no template, we should create an instance. Ex `build_gitlab_ci_service`
- public_send("build_#{service_name}_service")
+ public_send("build_#{service_name}_service") # rubocop:disable GitlabSecurity/PublicSend
else
Service.build_from_template(id, template)
end
@@ -921,14 +943,14 @@ class Project < ActiveRecord::Base
end
def execute_hooks(data, hooks_scope = :push_hooks)
- hooks.send(hooks_scope).each do |hook|
+ hooks.public_send(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend
hook.async_execute(data, hooks_scope.to_s)
end
end
def execute_services(data, hooks_scope = :push_hooks)
# Call only service hooks that are active for this scope
- services.send(hooks_scope).each do |service|
+ services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend
service.async_execute(data)
end
end
@@ -992,12 +1014,39 @@ class Project < ActiveRecord::Base
end
end
+ # Check if repository already exists on disk
+ def can_create_repository?
+ 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")
+ errors.add(:base, 'There is already a repository with that name on disk')
+ return false
+ end
+
+ true
+ 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)
+ repository.after_create
+ true
+ else
+ errors.add(:base, 'Failed to create repository via gitlab-shell')
+ false
+ end
+ end
+
def hook_attrs(backward: true)
attrs = {
name: name,
description: description,
web_url: web_url,
- avatar_url: avatar_url,
+ avatar_url: avatar_url(only_path: false),
git_ssh_url: ssh_url_to_repo,
git_http_url: http_url_to_repo,
namespace: namespace.name,
@@ -1046,13 +1095,16 @@ class Project < ActiveRecord::Base
end
def change_head(branch)
- repository.before_change_head
- repository.rugged.references.create('HEAD',
- "refs/heads/#{branch}",
- force: true)
- repository.copy_gitattributes(branch)
- repository.after_change_head
- reload_default_branch
+ if repository.branch_exists?(branch)
+ repository.before_change_head
+ repository.write_ref('HEAD', "refs/heads/#{branch}")
+ repository.copy_gitattributes(branch)
+ repository.after_change_head
+ reload_default_branch
+ else
+ errors.add(:base, "Could not change HEAD: branch '#{branch}' does not exist")
+ false
+ end
end
def forked_from?(project)
@@ -1071,6 +1123,7 @@ class Project < ActiveRecord::Base
!!repository.exists?
end
+ # update visibility_level of forks
def update_forks_visibility_level
return unless visibility_level < visibility_level_was
@@ -1143,7 +1196,11 @@ class Project < ActiveRecord::Base
end
def open_issues_count
- issues.opened.count
+ Projects::OpenIssuesCountService.new(self).count
+ end
+
+ def open_merge_requests_count
+ Projects::OpenMergeRequestsCountService.new(self).count
end
def visibility_level_allowed_as_fork?(level = self.visibility_level)
@@ -1198,13 +1255,18 @@ class Project < ActiveRecord::Base
end
def pages_path
- File.join(Settings.pages.path, disk_path)
+ # TODO: when we migrate Pages to work with new storage types, change here to use disk_path
+ File.join(Settings.pages.path, full_path)
end
def public_pages_path
File.join(pages_path, 'public')
end
+ def pages_available?
+ Gitlab.config.pages.enabled && !namespace.subgroup?
+ end
+
def remove_private_deploy_keys
exclude_keys_linked_to_other_projects = <<-SQL
NOT EXISTS (
@@ -1222,6 +1284,9 @@ class Project < ActiveRecord::Base
# TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal?
def remove_pages
+ # Projects with a missing namespace cannot have their pages removed
+ return unless namespace
+
::Projects::UpdatePagesConfigurationService.new(self).execute
# 1. We rename pages to temporary directory
@@ -1234,6 +1299,50 @@ class Project < ActiveRecord::Base
end
end
+ def rename_repo
+ new_full_path = build_full_path
+
+ Rails.logger.error "Attempting to rename #{full_path_was} -> #{new_full_path}"
+
+ if has_container_registry_tags?
+ Rails.logger.error "Project #{full_path_was} cannot be renamed because container registry tags are present!"
+
+ # we currently doesn't support renaming repository if it contains images in container registry
+ raise StandardError.new('Project cannot be renamed, because images are present in its container registry')
+ end
+
+ expire_caches_before_rename(full_path_was)
+
+ if storage.rename_repo
+ Gitlab::AppLogger.info "Project was renamed: #{full_path_was} -> #{new_full_path}"
+ rename_repo_notify!
+ after_rename_repo
+ else
+ Rails.logger.error "Repository could not be renamed: #{full_path_was} -> #{new_full_path}"
+
+ # if we cannot move namespace directory we should rollback
+ # db changes in order to prevent out of sync between db and fs
+ raise StandardError.new('repository cannot be renamed')
+ end
+ end
+
+ def rename_repo_notify!
+ send_move_instructions(full_path_was)
+ expires_full_path_cache
+
+ self.old_path_with_namespace = full_path_was
+ SystemHooksService.new.execute_hooks_for(self, :rename)
+
+ reload_repository!
+ end
+
+ def after_rename_repo
+ path_before_change = previous_changes['path'].first
+
+ Gitlab::UploadsTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
+ Gitlab::PagesTransfer.new.rename_project(path_before_change, self.path, namespace.full_path)
+ end
+
def running_or_pending_build_count(force: false)
Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
builds.running_or_pending.count(:all)
@@ -1280,12 +1389,20 @@ class Project < ActiveRecord::Base
status.zero?
end
+ def full_path_slug
+ Gitlab::Utils.slugify(full_path.to_s)
+ end
+
+ def has_ci?
+ repository.gitlab_ci_yml || auto_devops_enabled?
+ end
+
def predefined_variables
[
{ key: 'CI_PROJECT_ID', value: id.to_s, public: true },
{ key: 'CI_PROJECT_NAME', value: path, public: true },
{ key: 'CI_PROJECT_PATH', value: full_path, public: true },
- { key: 'CI_PROJECT_PATH_SLUG', value: full_path.parameterize, public: true },
+ { key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true },
{ key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: web_url, public: true }
]
@@ -1325,8 +1442,14 @@ class Project < ActiveRecord::Base
deployment_service.predefined_variables
end
+ def auto_devops_variables
+ return [] unless auto_devops_enabled?
+
+ auto_devops&.variables || []
+ end
+
def append_or_update_attribute(name, value)
- old_values = public_send(name.to_s)
+ old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend
if Project.reflect_on_association(name).try(:macro) == :has_many && old_values.any?
update_attribute(name, old_values + value)
@@ -1388,13 +1511,61 @@ class Project < ActiveRecord::Base
end
end
+ def multiple_issue_boards_available?(user)
+ feature_available?(:multiple_issue_boards, user)
+ end
+
+ def issue_board_milestone_available?(user = nil)
+ feature_available?(:issue_board_milestone, user)
+ end
+
+ def full_path_was
+ File.join(namespace.full_path, previous_changes['path'].first)
+ end
+
alias_method :name_with_namespace, :full_name
alias_method :human_name, :full_name
# @deprecated cannot remove yet because it has an index with its name in elasticsearch
alias_method :path_with_namespace, :full_path
+ def forks_count
+ Projects::ForksCountService.new(self).count
+ end
+
+ def legacy_storage?
+ self.storage_version.nil?
+ end
+
+ def renamed?
+ persisted? && path_changed?
+ end
+
private
+ def storage
+ @storage ||=
+ if self.storage_version && self.storage_version >= 1
+ Storage::HashedProject.new(self)
+ else
+ Storage::LegacyProject.new(self)
+ end
+ end
+
+ def use_hashed_storage
+ if self.new_record? && current_application_settings.hashed_storage_enabled
+ self.storage_version = LATEST_STORAGE_VERSION
+ end
+ end
+
+ # set last_activity_at to the same as created_at
+ def set_last_activity_at
+ update_column(:last_activity_at, self.created_at)
+ end
+
+ def set_last_repository_updated_at
+ update_column(:last_repository_updated_at, self.created_at)
+ end
+
def cross_namespace_reference?(from)
case from
when Project
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
new file mode 100644
index 00000000000..53731579e87
--- /dev/null
+++ b/app/models/project_auto_devops.rb
@@ -0,0 +1,11 @@
+class ProjectAutoDevops < ActiveRecord::Base
+ belongs_to :project
+
+ validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
+
+ def variables
+ variables = []
+ variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present?
+ variables
+ end
+end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index c8fabb16dc1..fb1db0255aa 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -55,7 +55,7 @@ class ProjectFeature < ActiveRecord::Base
end
def access_level(feature)
- public_send(ProjectFeature.access_level_attribute(feature))
+ public_send(ProjectFeature.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend
end
def builds_enabled?
@@ -80,7 +80,7 @@ class ProjectFeature < ActiveRecord::Base
# which cannot be higher than repository access level
def repository_children_level
validator = lambda do |field|
- level = public_send(field) || ProjectFeature::ENABLED
+ level = public_send(field) || ProjectFeature::ENABLED # rubocop:disable GitlabSecurity/PublicSend
not_allowed = level > repository_access_level
self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed
end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 6d1a321f651..818cfb01b14 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -101,9 +101,9 @@ class ChatNotificationService < Service
when "push", "tag_push"
ChatMessage::PushMessage.new(data)
when "issue"
- ChatMessage::IssueMessage.new(data) unless is_update?(data)
+ ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request"
- ChatMessage::MergeMessage.new(data) unless is_update?(data)
+ ChatMessage::MergeMessage.new(data) unless update?(data)
when "note"
ChatMessage::NoteMessage.new(data)
when "pipeline"
@@ -115,7 +115,7 @@ class ChatNotificationService < Service
def get_channel_field(event)
field_name = event_channel_name(event)
- self.public_send(field_name)
+ self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend
end
def build_event_channels
@@ -136,7 +136,7 @@ class ChatNotificationService < Service
project.web_url
end
- def is_update?(data)
+ def update?(data)
data[:object_attributes][:action] == 'update'
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index e3906943ecd..976d85246a8 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -53,7 +53,7 @@ class HipchatService < Service
return unless supported_events.include?(data[:object_kind])
message = create_message(data)
return unless message.present?
- gate[room].send('GitLab', message, message_options(data))
+ gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend
end
def test(data)
@@ -85,9 +85,9 @@ class HipchatService < Service
when "push", "tag_push"
create_push_message(data)
when "issue"
- create_issue_message(data) unless is_update?(data)
+ create_issue_message(data) unless update?(data)
when "merge_request"
- create_merge_request_message(data) unless is_update?(data)
+ create_merge_request_message(data) unless update?(data)
when "note"
create_note_message(data)
when "pipeline"
@@ -282,7 +282,7 @@ class HipchatService < Service
"<a href=\"#{project_url}\">#{project_name}</a>"
end
- def is_update?(data)
+ def update?(data)
data[:object_attributes][:action] == 'update'
end
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index dee99bbb859..8ba07173c74 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -24,6 +24,8 @@ class KubernetesService < DeploymentService
validates :token
end
+ before_validation :enforce_namespace_to_lower_case
+
validates :namespace,
allow_blank: true,
length: 1..63,
@@ -207,4 +209,8 @@ class KubernetesService < DeploymentService
max_session_time: current_application_settings.terminal_max_session_time
}
end
+
+ def enforce_namespace_to_lower_case
+ self.namespace = self.namespace&.downcase
+ end
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index aeaf63abab9..715b215d1db 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -14,7 +14,7 @@ class ProjectStatistics < ActiveRecord::Base
def refresh!(only: nil)
STATISTICS_COLUMNS.each do |column, generator|
if only.blank? || only.include?(column)
- public_send("update_#{column}")
+ public_send("update_#{column}") # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 674eacd28e8..09049824ff7 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -150,7 +150,7 @@ class ProjectTeam
end
def human_max_access(user_id)
- Gitlab::Access.options_with_owner.key(max_member_access(user_id))
+ Gitlab::Access.human_access(max_member_access(user_id))
end
# Determine the maximum access level for a group of users in bulk.
diff --git a/app/models/protectable_dropdown.rb b/app/models/protectable_dropdown.rb
index 122fbce257d..c96edc5a259 100644
--- a/app/models/protectable_dropdown.rb
+++ b/app/models/protectable_dropdown.rb
@@ -1,5 +1,9 @@
class ProtectableDropdown
+ REF_TYPES = %i[branches tags].freeze
+
def initialize(project, ref_type)
+ raise ArgumentError, "invalid ref type `#{ref_type}`" unless ref_type.in?(REF_TYPES)
+
@project = project
@ref_type = ref_type
end
@@ -16,7 +20,7 @@ class ProtectableDropdown
private
def refs
- @project.repository.public_send(@ref_type)
+ @project.repository.public_send(@ref_type) # rubocop:disable GitlabSecurity/PublicSend
end
def ref_names
@@ -24,7 +28,7 @@ class ProtectableDropdown
end
def protections
- @project.public_send("protected_#{@ref_type}")
+ @project.public_send("protected_#{@ref_type}") # rubocop:disable GitlabSecurity/PublicSend
end
def non_wildcard_protected_ref_names
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 5f0d0802ac9..89bfc5f9a9c 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -2,6 +2,8 @@ class ProtectedBranch < ActiveRecord::Base
include Gitlab::ShellAdapter
include ProtectedRef
+ extend Gitlab::CurrentSettings
+
protected_ref_access_levels :merge, :push
# Check if branch name is marked as protected in the system
diff --git a/app/models/push_event.rb b/app/models/push_event.rb
new file mode 100644
index 00000000000..23ffb0d4ea8
--- /dev/null
+++ b/app/models/push_event.rb
@@ -0,0 +1,81 @@
+class PushEvent < Event
+ # This validation exists so we can't accidentally use PushEvent with a
+ # 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
+ # should ensure the ID points to a valid project.
+ validates :project_id, presence: true
+
+ # These fields are also not used for push events, thus storing them would be a
+ # waste.
+ validates :target_id, absence: true
+ validates :target_type, absence: true
+
+ delegate :branch?, to: :push_event_payload
+ delegate :tag?, to: :push_event_payload
+ delegate :commit_from, to: :push_event_payload
+ delegate :commit_to, to: :push_event_payload
+ delegate :ref_type, to: :push_event_payload
+ delegate :commit_title, to: :push_event_payload
+
+ delegate :commit_count, to: :push_event_payload
+ alias_method :commits_count, :commit_count
+
+ def self.sti_name
+ PUSHED
+ end
+
+ def push?
+ true
+ end
+
+ def push_with_commits?
+ !!(commit_from && commit_to)
+ end
+
+ def valid_push?
+ push_event_payload.ref.present?
+ end
+
+ def new_ref?
+ push_event_payload.created?
+ end
+
+ def rm_ref?
+ push_event_payload.removed?
+ end
+
+ def md_ref?
+ !(rm_ref? || new_ref?)
+ end
+
+ def ref_name
+ push_event_payload.ref
+ end
+
+ alias_method :branch_name, :ref_name
+ alias_method :tag_name, :ref_name
+
+ def commit_id
+ commit_to || commit_from
+ end
+
+ def last_push_to_non_root?
+ branch? && project.default_branch != branch_name
+ end
+
+ def validate_push_action
+ return if action == PUSHED
+
+ errors.add(:action, "the action #{action.inspect} is not valid")
+ end
+end
diff --git a/app/models/push_event_payload.rb b/app/models/push_event_payload.rb
new file mode 100644
index 00000000000..6cdb1cd4fe9
--- /dev/null
+++ b/app/models/push_event_payload.rb
@@ -0,0 +1,22 @@
+class PushEventPayload < ActiveRecord::Base
+ include ShaAttribute
+
+ belongs_to :event, inverse_of: :push_event_payload
+
+ validates :event_id, :commit_count, :action, :ref_type, presence: true
+ validates :commit_title, length: { maximum: 70 }
+
+ sha_attribute :commit_from
+ sha_attribute :commit_to
+
+ enum action: {
+ created: 0,
+ removed: 1,
+ pushed: 2
+ }
+
+ enum ref_type: {
+ branch: 0,
+ tag: 1
+ }
+end
diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb
index 964175ddab8..31de204d824 100644
--- a/app/models/redirect_route.rb
+++ b/app/models/redirect_route.rb
@@ -8,5 +8,13 @@ class RedirectRoute < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
- scope :matching_path_and_descendants, -> (path) { where('redirect_routes.path = ? OR redirect_routes.path LIKE ?', path, "#{sanitize_sql_like(path)}/%") }
+ scope :matching_path_and_descendants, -> (path) do
+ wheres = if Gitlab::Database.postgresql?
+ 'LOWER(redirect_routes.path) = LOWER(?) OR LOWER(redirect_routes.path) LIKE LOWER(?)'
+ else
+ 'redirect_routes.path = ? OR redirect_routes.path LIKE ?'
+ end
+
+ where(wheres, path, "#{sanitize_sql_like(path)}/%")
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 3b5d0e00c70..035f85a0b46 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1,6 +1,18 @@
require 'securerandom'
class Repository
+ REF_MERGE_REQUEST = 'merge-requests'.freeze
+ REF_KEEP_AROUND = 'keep-around'.freeze
+ REF_ENVIRONMENTS = 'environments'.freeze
+
+ RESERVED_REFS_NAMES = %W[
+ heads
+ tags
+ #{REF_ENVIRONMENTS}
+ #{REF_KEEP_AROUND}
+ #{REF_ENVIRONMENTS}
+ ].freeze
+
include Gitlab::ShellAdapter
include RepositoryMirroring
@@ -8,7 +20,6 @@ class Repository
delegate :ref_name_for_sha, to: :raw_repository
- CommitError = Class.new(StandardError)
CreateTreeError = Class.new(StandardError)
# Methods that cache data from the Git repository.
@@ -48,7 +59,9 @@ class Repository
alias_method(original, name)
define_method(name) do
- cache_method_output(name, fallback: fallback, memoize_only: memoize_only) { __send__(original) }
+ cache_method_output(name, fallback: fallback, memoize_only: memoize_only) do
+ __send__(original) # rubocop:disable GitlabSecurity/PublicSend
+ end
end
end
@@ -58,12 +71,18 @@ class Repository
@project = project
end
+ def ==(other)
+ @disk_path == other.disk_path
+ end
+
def raw_repository
return nil unless full_path
@raw_repository ||= initialize_raw_repository
end
+ alias_method :raw, :raw_repository
+
# Return absolute path to repository
def path_to_repo
@path_to_repo ||= File.expand_path(
@@ -71,17 +90,8 @@ class Repository
)
end
- #
- # Git repository can contains some hidden refs like:
- # /refs/notes/*
- # /refs/git-as-svn/*
- # /refs/pulls/*
- # This refs by default not visible in project page and not cloned to client side.
- #
- # This method return true if repository contains some content visible in project page.
- #
- def has_visible_content?
- branch_count > 0
+ def inspect
+ "#<#{self.class.name}:#{@disk_path}>"
end
def commit(ref = 'HEAD')
@@ -156,32 +166,25 @@ class Repository
end
def add_branch(user, branch_name, ref)
- newrev = commit(ref).try(:sha)
-
- return false unless newrev
-
- GitOperationService.new(user, self).add_branch(branch_name, newrev)
+ branch = raw_repository.add_branch(branch_name, committer: user, target: ref)
after_create_branch
- find_branch(branch_name)
+
+ branch
+ rescue Gitlab::Git::Repository::InvalidRef
+ false
end
def add_tag(user, tag_name, target, message = nil)
- newrev = commit(target).try(:id)
- options = { message: message, tagger: user_to_committer(user) } if message
-
- return false unless newrev
-
- GitOperationService.new(user, self).add_tag(tag_name, newrev, options)
-
- find_tag(tag_name)
+ raw_repository.add_tag(tag_name, committer: user, target: target, message: message)
+ rescue Gitlab::Git::Repository::InvalidRef
+ false
end
def rm_branch(user, branch_name)
before_remove_branch
- branch = find_branch(branch_name)
- GitOperationService.new(user, self).rm_branch(branch)
+ raw_repository.rm_branch(branch_name, committer: user)
after_remove_branch
true
@@ -189,9 +192,8 @@ class Repository
def rm_tag(user, tag_name)
before_remove_tag
- tag = find_tag(tag_name)
- GitOperationService.new(user, self).rm_tag(tag)
+ raw_repository.rm_tag(tag_name, committer: user)
after_remove_tag
true
@@ -202,12 +204,18 @@ class Repository
end
def branch_exists?(branch_name)
- branch_names.include?(branch_name)
+ return false unless raw_repository
+
+ @branch_exists_memo ||= Hash.new do |hash, key|
+ hash[key] = raw_repository.branch_exists?(key)
+ end
+
+ @branch_exists_memo[branch_name]
end
def ref_exists?(ref)
- rugged.references.exist?(ref)
- rescue Rugged::ReferenceError
+ !!raw_repository&.ref_exists?(ref)
+ rescue ArgumentError
false
end
@@ -222,12 +230,12 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes)
begin
- rugged.references.create(keep_around_ref_name(sha), sha, force: true)
+ write_ref(keep_around_ref_name(sha), sha)
rescue Rugged::ReferenceError => ex
- Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
+ Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
rescue Rugged::OSError => ex
raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
- Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
+ Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
end
end
@@ -235,6 +243,10 @@ class Repository
ref_exists?(keep_around_ref_name(sha))
end
+ def write_ref(ref_path, sha)
+ rugged.references.create(ref_path, sha, force: true)
+ end
+
def diverging_commit_counts(branch)
root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
@@ -258,6 +270,7 @@ class Repository
def expire_branches_cache
expire_method_caches(%i(branch_names branch_count))
@local_branches = nil
+ @branch_exists_memo = nil
end
def expire_statistics_caches
@@ -298,7 +311,7 @@ class Repository
expire_method_caches(to_refresh)
- to_refresh.each { |method| send(method) }
+ to_refresh.each { |method| send(method) } # rubocop:disable GitlabSecurity/PublicSend
end
def expire_branch_cache(branch_name = nil)
@@ -437,9 +450,9 @@ class Repository
def method_missing(m, *args, &block)
if m == :lookup && !block_given?
lookup_cache[m] ||= {}
- lookup_cache[m][args.join(":")] ||= raw_repository.send(m, *args, &block)
+ lookup_cache[m][args.join(":")] ||= raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
else
- raw_repository.send(m, *args, &block)
+ raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
end
@@ -749,28 +762,42 @@ 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
+
+ newrev, should_run_after_create, should_run_after_create_branch = result
+
+ after_create if should_run_after_create
+ after_create_branch if should_run_after_create_branch
+
+ newrev
+ end
+
# rubocop:disable Metrics/ParameterLists
def multi_action(
user:, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
- GitOperationService.new(user, self).with_branch(
+ with_branch(
+ user,
branch_name,
start_branch_name: start_branch_name,
- start_project: start_project) do |start_commit|
+ start_repository: start_project.repository.raw_repository) do |start_commit|
index = Gitlab::Git::Index.new(raw_repository)
if start_commit
- index.read_tree(start_commit.raw_commit.tree)
+ index.read_tree(start_commit.rugged_commit.tree)
parents = [start_commit.sha]
else
parents = []
end
actions.each do |options|
- index.public_send(options.delete(:action), options)
+ index.public_send(options.delete(:action), options) # rubocop:disable GitlabSecurity/PublicSend
end
options = {
@@ -811,7 +838,8 @@ class Repository
end
def merge(user, source, merge_request, options = {})
- GitOperationService.new(user, self).with_branch(
+ with_branch(
+ user,
merge_request.target_branch) do |start_commit|
our_commit = start_commit.sha
their_commit = source
@@ -831,17 +859,18 @@ class Repository
merge_request.update(in_progress_merge_commit_sha: commit_id)
commit_id
end
- rescue Repository::CommitError # when merge_index.conflicts?
+ rescue Gitlab::Git::CommitError # when merge_index.conflicts?
false
end
def revert(
user, commit, branch_name,
start_branch_name: nil, start_project: project)
- GitOperationService.new(user, self).with_branch(
+ with_branch(
+ user,
branch_name,
start_branch_name: start_branch_name,
- start_project: start_project) do |start_commit|
+ start_repository: start_project.repository.raw_repository) do |start_commit|
revert_tree_id = check_revert_content(commit, start_commit.sha)
unless revert_tree_id
@@ -861,10 +890,11 @@ class Repository
def cherry_pick(
user, commit, branch_name,
start_branch_name: nil, start_project: project)
- GitOperationService.new(user, self).with_branch(
+ with_branch(
+ user,
branch_name,
start_branch_name: start_branch_name,
- start_project: start_project) do |start_commit|
+ 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
@@ -873,7 +903,7 @@ class Repository
committer = user_to_committer(user)
- create_commit(message: commit.message,
+ create_commit(message: commit.cherry_pick_message(user),
author: {
email: commit.author_email,
name: commit.author_name,
@@ -886,7 +916,7 @@ class Repository
end
def resolve_conflicts(user, branch_name, params)
- GitOperationService.new(user, self).with_branch(branch_name) do
+ with_branch(user, branch_name) do
committer = user_to_committer(user)
create_commit(params.merge(author: committer, committer: committer))
@@ -929,7 +959,7 @@ class Repository
if branch_commit
same_head = branch_commit.id == root_ref_commit.id
- !same_head && is_ancestor?(branch_commit.id, root_ref_commit.id)
+ !same_head && ancestor?(branch_commit.id, root_ref_commit.id)
else
nil
end
@@ -943,12 +973,12 @@ class Repository
nil
end
- def is_ancestor?(ancestor_id, descendant_id)
+ def ancestor?(ancestor_id, descendant_id)
return false if ancestor_id.nil? || descendant_id.nil?
Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
if is_enabled
- raw_repository.is_ancestor?(ancestor_id, descendant_id)
+ raw_repository.ancestor?(ancestor_id, descendant_id)
else
rugged_is_ancestor?(ancestor_id, descendant_id)
end
@@ -976,30 +1006,6 @@ class Repository
run_git(args).first.lines.map(&:strip)
end
- def with_repo_branch_commit(start_repository, start_branch_name)
- return yield(nil) if start_repository.empty_repo?
-
- branch_name_or_sha =
- if start_repository == self
- start_branch_name
- else
- tmp_ref = "refs/tmp/#{SecureRandom.hex}/head"
-
- fetch_ref(
- start_repository.path_to_repo,
- "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
- tmp_ref
- )
-
- start_repository.commit(start_branch_name).sha
- end
-
- yield(commit(branch_name_or_sha))
-
- ensure
- rugged.references.delete(tmp_ref) if tmp_ref
- end
-
def add_remote(name, url)
raw_repository.remote_add(name, url)
rescue Rugged::ConfigError
@@ -1014,12 +1020,15 @@ class Repository
end
def fetch_remote(remote, forced: false, no_tags: false)
- gitlab_shell.fetch_remote(repository_storage_path, disk_path, remote, forced: forced, no_tags: no_tags)
+ gitlab_shell.fetch_remote(raw_repository, remote, forced: forced, no_tags: no_tags)
+ end
+
+ def fetch_source_branch(source_repository, source_branch, local_ref)
+ raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref)
end
- def fetch_ref(source_path, source_ref, target_ref)
- args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
- run_git(args)
+ def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
+ raw_repository.compare_source_branch(target_branch_name, source_repository.raw_repository, source_branch_name, straight: straight)
end
def create_ref(ref, ref_path)
@@ -1100,12 +1109,6 @@ class Repository
private
- def run_git(args)
- circuit_breaker.perform do
- Gitlab::Popen.popen([Gitlab.config.git.bin_path, *args], path_to_repo)
- end
- end
-
def blob_data_at(sha, path)
blob = blob_at(sha, path)
return unless blob
@@ -1141,7 +1144,7 @@ class Repository
end
def keep_around_ref_name(sha)
- "refs/keep-around/#{sha}"
+ "refs/#{REF_KEEP_AROUND}/#{sha}"
end
def repository_event(event, tags = {})
@@ -1174,7 +1177,7 @@ class Repository
end
def initialize_raw_repository
- Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git')
+ Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false))
end
def circuit_breaker
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 09d5ff46618..9533aa7f555 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -10,6 +10,8 @@ class Snippet < ActiveRecord::Base
include Spammable
include Editable
+ extend Gitlab::CurrentSettings
+
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
cache_markdown_field :content
diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb
new file mode 100644
index 00000000000..fae1b64961a
--- /dev/null
+++ b/app/models/storage/hashed_project.rb
@@ -0,0 +1,42 @@
+module Storage
+ class HashedProject
+ attr_accessor :project
+ delegate :gitlab_shell, :repository_storage_path, to: :project
+
+ ROOT_PATH_PREFIX = '@hashed'.freeze
+
+ def initialize(project)
+ @project = project
+ end
+
+ # Base directory
+ #
+ # @return [String] directory where repository is stored
+ def base_dir
+ "#{ROOT_PATH_PREFIX}/#{disk_hash[0..1]}/#{disk_hash[2..3]}" if disk_hash
+ end
+
+ # Disk path is used to build repository and project's wiki path on disk
+ #
+ # @return [String] combination of base_dir and the repository own name without `.git` or `.wiki.git` extensions
+ def disk_path
+ "#{base_dir}/#{disk_hash}" if disk_hash
+ end
+
+ def ensure_storage_path_exists
+ gitlab_shell.add_namespace(repository_storage_path, base_dir)
+ end
+
+ def rename_repo
+ true
+ end
+
+ private
+
+ # Generates the hash for the project path and name on disk
+ # If you need to refer to the repository on disk, use the `#disk_path`
+ def disk_hash
+ @disk_hash ||= Digest::SHA2.hexdigest(project.id.to_s) if project.id
+ end
+ end
+end
diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb
new file mode 100644
index 00000000000..9d9e5e1d352
--- /dev/null
+++ b/app/models/storage/legacy_project.rb
@@ -0,0 +1,51 @@
+module Storage
+ class LegacyProject
+ attr_accessor :project
+ delegate :namespace, :gitlab_shell, :repository_storage_path, to: :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ # Base directory
+ #
+ # @return [String] directory where repository is stored
+ def base_dir
+ namespace.full_path
+ end
+
+ # Disk path is used to build repository and project's wiki path on disk
+ #
+ # @return [String] combination of base_dir and the repository own name without `.git` or `.wiki.git` extensions
+ def disk_path
+ project.full_path
+ end
+
+ def ensure_storage_path_exists
+ return unless namespace
+
+ gitlab_shell.add_namespace(repository_storage_path, base_dir)
+ end
+
+ def rename_repo
+ new_full_path = project.build_full_path
+
+ if gitlab_shell.mv_repository(repository_storage_path, project.full_path_was, new_full_path)
+ # If repository moved successfully we need to send update instructions to users.
+ # However we cannot allow rollback since we moved repository
+ # So we basically we mute exceptions in next actions
+ begin
+ gitlab_shell.mv_repository(repository_storage_path, "#{project.full_path_was}.wiki", "#{new_full_path}.wiki")
+ return true
+ rescue => e
+ Rails.logger.error "Exception renaming #{project.full_path_was} -> #{new_full_path}: #{e}"
+ # Returning false does not rollback after_* transaction but gives
+ # us information about failing some of tasks
+ return false
+ end
+ end
+
+ false
+ end
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 5148886eed7..d7549409b15 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,9 +2,11 @@ require 'carrierwave/orm/activerecord'
class User < ActiveRecord::Base
extend Gitlab::ConfigHelper
+ extend Gitlab::CurrentSettings
include Gitlab::ConfigHelper
include Gitlab::CurrentSettings
+ include Gitlab::SQL::Pattern
include Avatarable
include Referable
include Sortable
@@ -13,10 +15,12 @@ class User < ActiveRecord::Base
include IgnorableColumn
include FeatureGate
include CreatedAtFilterable
+ include IgnorableColumn
DEFAULT_NOTIFICATION_LEVEL = :participating
- ignore_column :authorized_projects_populated
+ ignore_column :external_email
+ ignore_column :email_provider
add_authentication_token_field :authentication_token
add_authentication_token_field :incoming_email_token
@@ -31,6 +35,7 @@ class User < ActiveRecord::Base
default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false
default_value_for :preferred_language, I18n.default_locale
+ default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
@@ -47,11 +52,6 @@ class User < ActiveRecord::Base
devise :lockable, :recoverable, :rememberable, :trackable,
:validatable, :omniauthable, :confirmable, :registerable
- # devise overrides #inspect, so we manually use the Referable one
- def inspect
- referable_inspect
- end
-
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
def update_tracked_fields!(request)
@@ -73,7 +73,7 @@ class User < ActiveRecord::Base
#
# Namespace for personal projects
- has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent
+ has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile
has_many :keys, -> do
@@ -88,6 +88,7 @@ class User < ActiveRecord::Base
has_many :identities, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
has_many :u2f_registrations, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :chat_names, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_one :user_synced_attributes_metadata, autosave: true
# Groups
has_many :members, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -148,6 +149,8 @@ class User < ActiveRecord::Base
uniqueness: { case_sensitive: false }
validate :namespace_uniq, if: :username_changed?
+ validate :namespace_move_dir_allowed, if: :username_changed?
+
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :unique_email, if: :email_changed?
validate :owns_notification_email, if: :notification_email_changed?
@@ -162,6 +165,7 @@ class User < ActiveRecord::Base
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) }
after_save :ensure_namespace_correct
after_commit :update_invalid_gpg_signatures, on: :update, if: -> { previous_changes.key?('email') }
after_initialize :set_projects_limit
@@ -256,11 +260,13 @@ class User < ActiveRecord::Base
end
def sort(method)
- case method.to_s
+ order_method = method || 'id_desc'
+
+ case order_method.to_s
when 'recent_sign_in' then order_recent_sign_in
when 'oldest_sign_in' then order_oldest_sign_in
else
- order_by(method)
+ order_by(order_method)
end
end
@@ -306,7 +312,7 @@ class User < ActiveRecord::Base
# Returns an ActiveRecord::Relation.
def search(query)
table = arel_table
- pattern = "%#{query}%"
+ pattern = User.to_pattern(query)
order = <<~SQL
CASE
@@ -368,7 +374,7 @@ class User < ActiveRecord::Base
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
- find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
+ Key.find_by(id: key_id)&.user
end
def find_by_full_path(path, follow_redirects: false)
@@ -487,6 +493,12 @@ class User < ActiveRecord::Base
end
end
+ def namespace_move_dir_allowed
+ if namespace&.any_project_has_container_registry_tags?
+ errors.add(:username, 'cannot be changed if a personal project has container registry tags.')
+ end
+ end
+
def avatar_type
unless avatar.image?
errors.add :avatar, "only images allowed"
@@ -528,7 +540,7 @@ class User < ActiveRecord::Base
union = Gitlab::SQL::Union
.new([groups.select(:id), authorized_projects.select(:namespace_id)])
- Group.where("namespaces.id IN (#{union.to_sql})")
+ Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
end
# Returns a relation of groups the user has access to, including their parent
@@ -598,7 +610,7 @@ class User < ActiveRecord::Base
end
def require_personal_access_token_creation_for_git_auth?
- return false if allow_password_authentication? || ldap_user?
+ return false if current_application_settings.password_authentication_enabled? || ldap_user?
PersonalAccessTokensFinder.new(user: self, impersonation: false, state: 'active').execute.none?
end
@@ -639,11 +651,6 @@ class User < ActiveRecord::Base
@personal_projects_count ||= personal_projects.count
end
- def projects_limit_percent
- return 100 if projects_limit.zero?
- (personal_projects.count.to_f / projects_limit) * 100
- 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)
@@ -661,10 +668,6 @@ class User < ActiveRecord::Base
end
end
- def projects_sorted_by_activity
- authorized_projects.sorted_by_activity
- end
-
def several_namespaces?
owned_groups.any? || masters_groups.any?
end
@@ -718,9 +721,9 @@ class User < ActiveRecord::Base
end
def sanitize_attrs
- %w[username skype linkedin twitter].each do |attr|
- value = public_send(attr)
- public_send("#{attr}=", Sanitize.clean(value)) if value.present?
+ %i[skype linkedin twitter].each do |attr|
+ value = self[attr]
+ self[attr] = Sanitize.clean(value) if value.present?
end
end
@@ -779,7 +782,7 @@ class User < ActiveRecord::Base
def with_defaults
User.defaults.each do |k, v|
- public_send("#{k}=", v)
+ public_send("#{k}=", v) # rubocop:disable GitlabSecurity/PublicSend
end
self
@@ -825,7 +828,7 @@ class User < ActiveRecord::Base
{
name: name,
username: username,
- avatar_url: avatar_url
+ avatar_url: avatar_url(only_path: false)
}
end
@@ -834,7 +837,12 @@ class User < ActiveRecord::Base
create_namespace!(path: username, name: username) unless namespace
if username_changed?
- namespace.update_attributes(path: username, name: username)
+ unless namespace.update_attributes(path: username, name: username)
+ namespace.errors.each do |attribute, message|
+ self.errors.add(:"namespace_#{attribute}", message)
+ end
+ raise ActiveRecord::RecordInvalid.new(namespace)
+ end
end
end
@@ -919,7 +927,7 @@ class User < ActiveRecord::Base
def ci_authorized_runners
@ci_authorized_runners ||= begin
runner_ids = Ci::RunnerProject
- .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})")
+ .where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
.select(:runner_id)
Ci::Runner.specific.where(id: runner_ids)
end
@@ -1040,6 +1048,26 @@ 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
+
+ attributes = Gitlab.config.omniauth.sync_profile_attributes
+
+ if attributes.is_a?(Array)
+ attributes.include?(attribute.to_s)
+ else
+ attributes
+ end
+ end
+
+ def read_only_attribute?(attribute)
+ user_synced_attributes_metadata&.read_only?(attribute)
+ end
+
protected
# override, from Devise::Validatable
@@ -1061,7 +1089,8 @@ class User < ActiveRecord::Base
# Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
def send_devise_notification(notification, *args)
- devise_mailer.send(notification, self, *args).deliver_later
+ return true unless can?(:receive_notifications)
+ devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
end
# This works around a bug in Devise 4.2.0 that erroneously causes a user to
diff --git a/app/models/user_synced_attributes_metadata.rb b/app/models/user_synced_attributes_metadata.rb
new file mode 100644
index 00000000000..9f374304164
--- /dev/null
+++ b/app/models/user_synced_attributes_metadata.rb
@@ -0,0 +1,25 @@
+class UserSyncedAttributesMetadata < ActiveRecord::Base
+ belongs_to :user
+
+ validates :user, presence: true
+
+ SYNCABLE_ATTRIBUTES = %i[name email location].freeze
+
+ def read_only?(attribute)
+ Gitlab.config.omniauth.sync_profile_from_provider && synced?(attribute)
+ end
+
+ def read_only_attributes
+ return [] unless Gitlab.config.omniauth.sync_profile_from_provider
+
+ SYNCABLE_ATTRIBUTES.select { |key| synced?(key) }
+ end
+
+ def synced?(attribute)
+ read_attribute("#{attribute}_synced")
+ end
+
+ def set_attribute_synced(attribute, value)
+ write_attribute("#{attribute}_synced", value)
+ end
+end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 5c7c2204374..f2315bb3dbb 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -84,7 +84,7 @@ class WikiPage
# The formatted title of this page.
def title
if @attributes[:title]
- self.class.unhyphenize(@attributes[:title])
+ CGI.unescape_html(self.class.unhyphenize(@attributes[:title]))
else
""
end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index a605a3457c8..8fa7b2753c7 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -1,8 +1,6 @@
require_dependency 'declarative_policy'
class BasePolicy < DeclarativePolicy::Base
- include Gitlab::CurrentSettings
-
desc "User is an instance admin"
with_options scope: :user, score: 0
condition(:admin) { @user&.admin? }
@@ -15,6 +13,6 @@ class BasePolicy < DeclarativePolicy::Base
desc "The application is restricted from public visibility"
condition(:restricted_public_level, scope: :global) do
- current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
+ Gitlab::CurrentSettings.current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 6defab75fce..420991ff6d6 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -13,6 +13,13 @@ class GroupPolicy < BasePolicy
condition(:master) { access_level >= GroupMember::MASTER }
condition(:reporter) { access_level >= GroupMember::REPORTER }
+ condition(:nested_groups_supported, scope: :global) { Group.supports_nested_groups? }
+
+ condition(:has_parent, scope: :subject) { @subject.has_parent? }
+ condition(:share_with_group_locked, scope: :subject) { @subject.share_with_group_lock? }
+ condition(:parent_share_with_group_locked, scope: :subject) { @subject.parent&.share_with_group_lock? }
+ condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
+
condition(:has_projects) do
GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
end
@@ -42,7 +49,7 @@ class GroupPolicy < BasePolicy
enable :change_visibility_level
end
- rule { owner & can_create_group }.enable :create_subgroup
+ rule { owner & nested_groups_supported }.enable :create_subgroup
rule { public_group | logged_in_viewable }.enable :view_globally
@@ -52,6 +59,8 @@ class GroupPolicy < BasePolicy
rule { ~can?(:view_globally) }.prevent :request_access
rule { has_access }.prevent :request_access
+ rule { owner & (~share_with_group_locked | ~has_parent | ~parent_share_with_group_locked | can_change_parent_share_with_group_lock) }.enable :change_share_with_group_lock
+
def access_level
return GroupMember::NO_ACCESS if @user.nil?
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 0133091db57..a925fac7d3e 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -17,13 +17,13 @@ class ProjectPolicy < BasePolicy
desc "Project has public builds enabled"
condition(:public_builds, scope: :subject) { project.public_builds? }
- # For guest access we use #is_team_member? so we can use
+ # For guest access we use #team_member? so we can use
# project.members, which gets cached in subject scope.
# This is safe because team_access_level is guaranteed
# by ProjectAuthorization's validation to be at minimum
# GUEST
desc "User has guest access"
- condition(:guest) { is_team_member? }
+ condition(:guest) { team_member? }
desc "User has reporter access"
condition(:reporter) { team_access_level >= Gitlab::Access::REPORTER }
@@ -293,7 +293,7 @@ class ProjectPolicy < BasePolicy
private
- def is_team_member?
+ def team_member?
return false if @user.nil?
greedy_load_subject = false
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index c495c3f39bb..255475e1fe6 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -17,5 +17,16 @@ module Ci
"Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
end
end
+
+ def trigger_variables
+ return [] unless trigger_request
+
+ @trigger_variables ||=
+ if pipeline.variables.any?
+ pipeline.variables.map(&:to_runner_variable)
+ else
+ trigger_request.user_variables
+ end
+ end
end
end
diff --git a/app/serializers/analytics_build_entity.rb b/app/serializers/analytics_build_entity.rb
index ad7ad020b03..bdc22d71202 100644
--- a/app/serializers/analytics_build_entity.rb
+++ b/app/serializers/analytics_build_entity.rb
@@ -35,6 +35,6 @@ class AnalyticsBuildEntity < Grape::Entity
private
def url_to(route, build, id = nil)
- public_send("#{route}_url", build.project.namespace, build.project, id || build)
+ public_send("#{route}_url", build.project.namespace, build.project, id || build) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/serializers/analytics_issue_entity.rb b/app/serializers/analytics_issue_entity.rb
index 44c50f18613..b7d95ea020f 100644
--- a/app/serializers/analytics_issue_entity.rb
+++ b/app/serializers/analytics_issue_entity.rb
@@ -24,6 +24,6 @@ class AnalyticsIssueEntity < Grape::Entity
private
def url_to(route, id)
- public_send("#{route}_url", request.project.namespace, request.project, id)
+ public_send("#{route}_url", request.project.namespace, request.project, id) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/serializers/award_emoji_entity.rb b/app/serializers/award_emoji_entity.rb
new file mode 100644
index 00000000000..6e03cd02392
--- /dev/null
+++ b/app/serializers/award_emoji_entity.rb
@@ -0,0 +1,4 @@
+class AwardEmojiEntity < Grape::Entity
+ expose :name
+ expose :user, using: API::Entities::UserSafe
+end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
new file mode 100644
index 00000000000..0a92e3f8167
--- /dev/null
+++ b/app/serializers/discussion_entity.rb
@@ -0,0 +1,10 @@
+class DiscussionEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id, :reply_id
+ expose :expanded?, as: :expanded
+
+ expose :notes, using: NoteEntity
+
+ expose :individual_note?, as: :individual_note
+end
diff --git a/app/serializers/discussion_serializer.rb b/app/serializers/discussion_serializer.rb
new file mode 100644
index 00000000000..ed5e1224bb2
--- /dev/null
+++ b/app/serializers/discussion_serializer.rb
@@ -0,0 +1,3 @@
+class DiscussionSerializer < BaseSerializer
+ entity DiscussionEntity
+end
diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb
index bd5211b8e58..61c7a428745 100644
--- a/app/serializers/issuable_entity.rb
+++ b/app/serializers/issuable_entity.rb
@@ -15,4 +15,6 @@ class IssuableEntity < Grape::Entity
expose :total_time_spent
expose :human_time_estimate
expose :human_total_time_spent
+ expose :milestone, using: API::Entities::Milestone
+ expose :labels, using: LabelEntity
end
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index c189a4992da..0d6feb78173 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -7,10 +7,26 @@ class IssueEntity < IssuableEntity
expose :due_date
expose :moved_to_id
expose :project_id
- expose :milestone, using: API::Entities::Milestone
- expose :labels, using: LabelEntity
expose :web_url do |issue|
project_issue_path(issue.project, issue)
end
+
+ expose :current_user do
+ expose :can_create_note do |issue|
+ can?(request.current_user, :create_note, issue.project)
+ end
+
+ expose :can_update do |issue|
+ can?(request.current_user, :update_issue, issue)
+ end
+ end
+
+ expose :create_note_path do |issue|
+ project_notes_path(issue.project, target_type: 'issue', target_id: issue.id)
+ end
+
+ expose :preview_note_path do |issue|
+ preview_markdown_path(issue.project, quick_actions_target_type: 'Issue', quick_actions_target_id: issue.id)
+ end
end
diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb
index d6de43bcbcb..72e56a2c77f 100644
--- a/app/serializers/job_entity.rb
+++ b/app/serializers/job_entity.rb
@@ -46,6 +46,6 @@ class JobEntity < Grape::Entity
end
def path_to(route, build)
- send("#{route}_path", build.project.namespace, build.project, build)
+ send("#{route}_path", build.project.namespace, build.project, build) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/serializers/note_attachment_entity.rb b/app/serializers/note_attachment_entity.rb
new file mode 100644
index 00000000000..1ad50568ab9
--- /dev/null
+++ b/app/serializers/note_attachment_entity.rb
@@ -0,0 +1,5 @@
+class NoteAttachmentEntity < Grape::Entity
+ expose :url
+ expose :filename
+ expose :image?, as: :image
+end
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
new file mode 100644
index 00000000000..7d50e0ff10d
--- /dev/null
+++ b/app/serializers/note_entity.rb
@@ -0,0 +1,60 @@
+class NoteEntity < API::Entities::Note
+ include RequestAwareEntity
+
+ expose :type
+
+ expose :author, using: NoteUserEntity
+
+ expose :human_access do |note|
+ note.project.team.human_max_access(note.author_id)
+ end
+
+ unexpose :note, as: :body
+ expose :note
+
+ expose :redacted_note_html, as: :note_html
+
+ expose :last_edited_at, if: -> (note, _) { note.edited? }
+ expose :last_edited_by, using: NoteUserEntity, if: -> (note, _) { note.edited? }
+
+ expose :current_user do
+ expose :can_edit do |note|
+ Ability.can_edit_note?(request.current_user, note)
+ end
+ end
+
+ expose :system_note_icon_name, if: -> (note, _) { note.system? } do |note|
+ SystemNoteHelper.system_note_icon_name(note)
+ end
+
+ expose :discussion_id do |note|
+ note.discussion_id(request.noteable)
+ end
+
+ expose :emoji_awardable?, as: :emoji_awardable
+ expose :award_emoji, if: -> (note, _) { note.emoji_awardable? }, using: AwardEmojiEntity
+ expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note|
+ if note.for_personal_snippet?
+ toggle_award_emoji_snippet_note_path(note.noteable, note)
+ else
+ toggle_award_emoji_project_note_path(note.project, note.id)
+ end
+ end
+
+ expose :report_abuse_path do |note|
+ new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note))
+ end
+
+ expose :path do |note|
+ if note.for_personal_snippet?
+ snippet_note_path(note.noteable, note)
+ else
+ project_note_path(note.project, note)
+ end
+ end
+
+ expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
+ expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note|
+ delete_attachment_project_note_path(note.project, note)
+ end
+end
diff --git a/app/serializers/note_serializer.rb b/app/serializers/note_serializer.rb
new file mode 100644
index 00000000000..2afe40d7a34
--- /dev/null
+++ b/app/serializers/note_serializer.rb
@@ -0,0 +1,3 @@
+class NoteSerializer < BaseSerializer
+ entity NoteEntity
+end
diff --git a/app/serializers/note_user_entity.rb b/app/serializers/note_user_entity.rb
new file mode 100644
index 00000000000..7289f3a0222
--- /dev/null
+++ b/app/serializers/note_user_entity.rb
@@ -0,0 +1,3 @@
+class NoteUserEntity < UserEntity
+ unexpose :web_url
+end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index c4f000b0ca3..357fc71f877 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -16,6 +16,7 @@ class PipelineEntity < Grape::Entity
expose :flags do
expose :latest?, as: :latest
expose :stuck?, as: :stuck
+ expose :auto_devops_source?, as: :auto_devops
expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable
diff --git a/app/serializers/project_entity.rb b/app/serializers/project_entity.rb
index dc283ba3e7a..b3e5fd21e97 100644
--- a/app/serializers/project_entity.rb
+++ b/app/serializers/project_entity.rb
@@ -1,6 +1,6 @@
class ProjectEntity < Grape::Entity
include RequestAwareEntity
-
+
expose :id
expose :name
diff --git a/app/serializers/tree_root_entity.rb b/app/serializers/tree_root_entity.rb
index 23b65aa4a4c..69702ae1493 100644
--- a/app/serializers/tree_root_entity.rb
+++ b/app/serializers/tree_root_entity.rb
@@ -1,8 +1,21 @@
# TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`.
class TreeRootEntity < Grape::Entity
+ include RequestAwareEntity
+
expose :path
-
+
expose :trees, using: TreeEntity
expose :blobs, using: BlobEntity
expose :submodules, using: SubmoduleEntity
+
+ expose :parent_tree_url do |tree|
+ path = tree.path.sub(%r{\A/}, '')
+ next unless path.present?
+
+ path_segments = path.split('/')
+ path_segments.pop
+ parent_tree_path = path_segments.join('/')
+
+ project_tree_path(request.project, File.join(request.ref, parent_tree_path))
+ end
end
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
new file mode 100644
index 00000000000..49a71ebac61
--- /dev/null
+++ b/app/serializers/user_serializer.rb
@@ -0,0 +1,3 @@
+class UserSerializer < BaseSerializer
+ entity UserEntity
+end
diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb
index 8e11a2a36a7..aa6f0e841c9 100644
--- a/app/services/akismet_service.rb
+++ b/app/services/akismet_service.rb
@@ -1,4 +1,6 @@
class AkismetService
+ include Gitlab::CurrentSettings
+
attr_accessor :owner, :text, :options
def initialize(owner, text, options = {})
@@ -7,7 +9,7 @@ class AkismetService
@options = options
end
- def is_spam?
+ def spam?
return false unless akismet_enabled?
params = {
@@ -58,7 +60,7 @@ class AkismetService
}
begin
- akismet_client.public_send(type, options[:ip_address], options[:user_agent], params)
+ akismet_client.public_send(type, options[:ip_address], options[:user_agent], params) # rubocop:disable GitlabSecurity/PublicSend
true
rescue => e
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 7dae5880931..9a636346899 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -1,6 +1,6 @@
module Auth
class ContainerRegistryAuthenticationService < BaseService
- include Gitlab::CurrentSettings
+ extend Gitlab::CurrentSettings
AUDIENCE = 'container_registry'.freeze
diff --git a/app/services/boards/base_service.rb b/app/services/boards/base_service.rb
new file mode 100644
index 00000000000..72822ffffa1
--- /dev/null
+++ b/app/services/boards/base_service.rb
@@ -0,0 +1,10 @@
+module Boards
+ class BaseService < ::BaseService
+ # Parent can either a group or a project
+ attr_accessor :parent, :current_user, :params
+
+ def initialize(parent, user, params = {})
+ @parent, @current_user, @params = parent, user, params.dup
+ end
+ end
+end
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
index 9eedb9e65a2..bd0bb387662 100644
--- a/app/services/boards/create_service.rb
+++ b/app/services/boards/create_service.rb
@@ -1,5 +1,5 @@
module Boards
- class CreateService < BaseService
+ class CreateService < Boards::BaseService
def execute
create_board! if can_create_board?
end
@@ -7,11 +7,11 @@ module Boards
private
def can_create_board?
- project.boards.size == 0
+ parent.boards.size == 0
end
def create_board!
- board = project.boards.create(params)
+ board = parent.boards.create(params)
if board.persisted?
board.lists.create(list_type: :backlog)
diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb
index c0d7ff5b585..7c4a79f555e 100644
--- a/app/services/boards/issues/create_service.rb
+++ b/app/services/boards/issues/create_service.rb
@@ -1,6 +1,14 @@
module Boards
module Issues
- class CreateService < BaseService
+ class CreateService < Boards::BaseService
+ attr_accessor :project
+
+ def initialize(parent, project, user, params = {})
+ @project = project
+
+ super(parent, user, params)
+ end
+
def execute
create_issue(params.merge(label_ids: [list.label_id]))
end
@@ -8,7 +16,7 @@ module Boards
private
def board
- @board ||= project.boards.find(params.delete(:board_id))
+ @board ||= parent.boards.find(params.delete(:board_id))
end
def list
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index eb345fead2d..d85d93e251b 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -1,6 +1,6 @@
module Boards
module Issues
- class ListService < BaseService
+ class ListService < Boards::BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless movable_list? || closed_list?
@@ -11,7 +11,7 @@ module Boards
private
def board
- @board ||= project.boards.find(params[:board_id])
+ @board ||= parent.boards.find(params[:board_id])
end
def list
@@ -33,14 +33,14 @@ module Boards
end
def filter_params
- set_project
+ set_parent
set_state
params
end
- def set_project
- params[:project_id] = project.id
+ def set_parent
+ params[:project_id] = parent.id
end
def set_state
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index ecabb2a48e4..797d6df7c1a 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -1,17 +1,17 @@
module Boards
module Issues
- class MoveService < BaseService
+ class MoveService < Boards::BaseService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
return false if issue_params.empty?
- update_service.execute(issue)
+ update(issue)
end
private
def board
- @board ||= project.boards.find(params[:board_id])
+ @board ||= parent.boards.find(params[:board_id])
end
def move_between_lists?
@@ -27,8 +27,8 @@ module Boards
@moving_to_list ||= board.lists.find_by(id: params[:to_list_id])
end
- def update_service
- ::Issues::UpdateService.new(project, current_user, issue_params)
+ def update(issue)
+ ::Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue)
end
def issue_params
@@ -42,7 +42,7 @@ module Boards
)
end
- attrs[:move_between_iids] = move_between_iids if move_between_iids
+ attrs[:move_between_ids] = move_between_ids if move_between_ids
attrs
end
@@ -61,16 +61,16 @@ module Boards
if moving_to_list.movable?
moving_from_list.label_id
else
- Label.on_project_boards(project.id).pluck(:label_id)
+ Label.on_project_boards(parent.id).pluck(:label_id)
end
Array(label_ids).compact
end
- def move_between_iids
- return unless params[:move_after_iid] || params[:move_before_iid]
+ def move_between_ids
+ return unless params[:move_after_id] || params[:move_before_id]
- [params[:move_after_iid], params[:move_before_iid]]
+ [params[:move_after_id], params[:move_before_id]]
end
end
end
diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb
index 84f1fc3a4e2..6d0dd0a9f99 100644
--- a/app/services/boards/list_service.rb
+++ b/app/services/boards/list_service.rb
@@ -1,14 +1,14 @@
module Boards
- class ListService < BaseService
+ class ListService < Boards::BaseService
def execute
- create_board! if project.boards.empty?
- project.boards
+ create_board! if parent.boards.empty?
+ parent.boards
end
private
def create_board!
- Boards::CreateService.new(project, current_user).execute
+ Boards::CreateService.new(parent, current_user).execute
end
end
end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index fe0d762ccd2..183556a1d6b 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -1,19 +1,18 @@
module Boards
module Lists
- class CreateService < BaseService
+ class CreateService < Boards::BaseService
def execute(board)
List.transaction do
- label = available_labels.find(params[:label_id])
+ label = available_labels_for(board).find(params[:label_id])
position = next_position(board)
-
create_list(board, label, position)
end
end
private
- def available_labels
- LabelsFinder.new(current_user, project_id: project.id).execute
+ def available_labels_for(board)
+ LabelsFinder.new(current_user, project_id: parent.id).execute
end
def next_position(board)
diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb
index f986e05944c..d75c5fd3dc6 100644
--- a/app/services/boards/lists/destroy_service.rb
+++ b/app/services/boards/lists/destroy_service.rb
@@ -1,6 +1,6 @@
module Boards
module Lists
- class DestroyService < BaseService
+ class DestroyService < Boards::BaseService
def execute(list)
return false unless list.destroyable?
diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb
index 939f9bfd068..05d4ab5dbcc 100644
--- a/app/services/boards/lists/generate_service.rb
+++ b/app/services/boards/lists/generate_service.rb
@@ -1,6 +1,6 @@
module Boards
module Lists
- class GenerateService < BaseService
+ class GenerateService < Boards::BaseService
def execute(board)
return false unless board.lists.movable.empty?
@@ -15,11 +15,11 @@ module Boards
def create_list(board, params)
label = find_or_create_label(params)
- Lists::CreateService.new(project, current_user, label_id: label.id).execute(board)
+ Lists::CreateService.new(parent, current_user, label_id: label.id).execute(board)
end
def find_or_create_label(params)
- ::Labels::FindOrCreateService.new(current_user, project, params).execute
+ ::Labels::FindOrCreateService.new(current_user, parent, params).execute
end
def label_params
diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb
index df2a01a69e5..e57c95294af 100644
--- a/app/services/boards/lists/list_service.rb
+++ b/app/services/boards/lists/list_service.rb
@@ -1,6 +1,6 @@
module Boards
module Lists
- class ListService < BaseService
+ class ListService < Boards::BaseService
def execute(board)
board.lists.create(list_type: :backlog) unless board.lists.backlog.exists?
diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb
index f2a68865f7b..7d0730e8332 100644
--- a/app/services/boards/lists/move_service.rb
+++ b/app/services/boards/lists/move_service.rb
@@ -1,6 +1,6 @@
module Boards
module Lists
- class MoveService < BaseService
+ class MoveService < Boards::BaseService
def execute(list)
@board = list.board
@old_position = list.position
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 884b681ff81..d20de9b16a4 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -12,12 +12,13 @@ module Ci
tag: tag?,
trigger_requests: Array(trigger_request),
user: current_user,
- pipeline_schedule: schedule
+ pipeline_schedule: schedule,
+ protected: project.protected_for?(ref)
)
- result = validate(current_user || trigger_request.trigger.owner,
- ignore_skip_ci: ignore_skip_ci,
- save_on_errors: save_on_errors)
+ result = validate_project_and_git_items ||
+ validate_pipeline(ignore_skip_ci: ignore_skip_ci,
+ save_on_errors: save_on_errors)
return result if result
@@ -46,13 +47,13 @@ module Ci
private
- def validate(triggering_user, ignore_skip_ci:, save_on_errors:)
+ def validate_project_and_git_items
unless project.builds_enabled?
return error('Pipeline is disabled')
end
- unless allowed_to_trigger_pipeline?(triggering_user)
- if can?(triggering_user, :create_pipeline, project)
+ 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')
@@ -66,7 +67,9 @@ module Ci
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")
@@ -84,25 +87,25 @@ module Ci
end
end
- def allowed_to_trigger_pipeline?(triggering_user)
- if triggering_user
- allowed_to_create?(triggering_user)
+ 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
end
- def allowed_to_create?(triggering_user)
- access = Gitlab::UserAccess.new(triggering_user, project: project)
+ def allowed_to_create?
+ return unless can?(current_user, :create_pipeline, project)
- can?(triggering_user, :create_pipeline, 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
+ 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
end
def update_merge_requests_head_pipeline
@@ -176,9 +179,14 @@ module Ci
end
def error(message, save: false)
- pipeline.errors.add(:base, message)
- pipeline.drop if save
- pipeline
+ pipeline.tap do
+ pipeline.errors.add(:base, message)
+
+ if save
+ pipeline.drop
+ update_merge_requests_head_pipeline
+ end
+ end
end
def pipeline_created_counter
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
deleted file mode 100644
index b2aa457bbd5..00000000000
--- a/app/services/ci/create_trigger_request_service.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# This class is deprecated because we're closing Ci::TriggerRequest.
-# New class is PipelineTriggerService (app/services/ci/pipeline_trigger_service.rb)
-# which is integrated with Ci::PipelineVariable instaed of Ci::TriggerRequest.
-# We remove this class after we removed v1 and v3 API. This class is still being
-# referred by such legacy code.
-module Ci
- module CreateTriggerRequestService
- Result = Struct.new(:trigger_request, :pipeline)
-
- def self.execute(project, trigger, ref, variables = nil)
- trigger_request = trigger.trigger_requests.create(variables: variables)
-
- pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref)
- .execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request)
-
- Result.new(trigger_request, pipeline)
- end
- end
-end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index fc87bd6a659..b8db709211a 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -77,7 +77,9 @@ module Ci
end
def new_builds
- Ci::Build.pending.unstarted
+ builds = Ci::Build.pending.unstarted
+ builds = builds.ref_protected if runner.ref_protected?
+ builds
end
def shared_runner_build_limits_feature_enabled?
@@ -85,13 +87,13 @@ module Ci
end
def register_failure
- failed_attempt_counter.increase
- attempt_counter.increase
+ failed_attempt_counter.increment
+ attempt_counter.increment
end
def register_success(job)
job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at)
- attempt_counter.increase
+ attempt_counter.increment
end
def failed_attempt_counter
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 6372e5755db..d67b9f5cc56 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -3,7 +3,7 @@ module Ci
CLONE_ACCESSORS = %i[pipeline project ref tag options commands name
allow_failure stage_id stage stage_idx trigger_request
yaml_variables when environment coverage_regex
- description tag_list].freeze
+ description tag_list protected].freeze
def execute(build)
reprocess!(build).tap do |new_build|
@@ -23,7 +23,7 @@ module Ci
end
attributes = CLONE_ACCESSORS.map do |attribute|
- [attribute, build.send(attribute)]
+ [attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend
end
attributes.push([:user, current_user])
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index a48d6a976f0..85c2fcf9ea6 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -11,6 +11,7 @@ module Commits
def commit_change(action)
raise NotImplementedError unless repository.respond_to?(action)
+ # rubocop:disable GitlabSecurity/PublicSend
repository.public_send(
action,
current_user,
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index c58f04a252b..f96f2931508 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -17,7 +17,7 @@ module Commits
new_commit = create_commit!
success(result: new_commit)
- rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Repository::CommitError, GitHooksService::PreReceiveError => ex
+ rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex
error(ex.message)
end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index a5ae4927412..53f16a236d2 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -11,26 +11,8 @@ class CompareService
end
def execute(target_project, target_branch, straight: false)
- # If compare with other project we need to fetch ref first
- target_project.repository.with_repo_branch_commit(
- start_project.repository,
- start_branch_name) do |commit|
- break unless commit
+ raw_compare = target_project.repository.compare_source_branch(target_branch, start_project.repository, start_branch_name, straight: straight)
- compare(commit.sha, target_project, target_branch, straight: straight)
- end
- end
-
- private
-
- def compare(source_sha, target_project, target_branch, straight:)
- raw_compare = Gitlab::Git::Compare.new(
- target_project.repository.raw_repository,
- target_branch,
- source_sha,
- straight: straight
- )
-
- Compare.new(raw_compare, target_project, straight: straight)
+ Compare.new(raw_compare, target_project, straight: straight) if raw_compare
end
end
diff --git a/app/services/concerns/update_visibility_level.rb b/app/services/concerns/update_visibility_level.rb
new file mode 100644
index 00000000000..536fcc6acce
--- /dev/null
+++ b/app/services/concerns/update_visibility_level.rb
@@ -0,0 +1,15 @@
+module UpdateVisibilityLevel
+ def valid_visibility_level_change?(target, new_visibility)
+ # check that user is allowed to set specified visibility_level
+ if new_visibility && new_visibility.to_i != target.visibility_level
+ unless can?(current_user, :change_visibility_level, target) &&
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+
+ deny_visibility_level(target, new_visibility)
+ return false
+ end
+ end
+
+ true
+ end
+end
diff --git a/app/services/concerns/users/new_user_notifier.rb b/app/services/concerns/users/new_user_notifier.rb
new file mode 100644
index 00000000000..231693ce7a9
--- /dev/null
+++ b/app/services/concerns/users/new_user_notifier.rb
@@ -0,0 +1,9 @@
+module Users
+ module NewUserNotifier
+ def notify_new_user(user, reset_token)
+ log_info("User \"#{user.name}\" (#{user.email}) was created")
+ notification_service.new_user(user, reset_token) if reset_token
+ system_hook_service.execute_hooks_for(user, :create)
+ end
+ end
+end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index 673ed02f952..0ba6a0ac6b5 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -14,7 +14,7 @@ class CreateBranchService < BaseService
else
error('Invalid reference name')
end
- rescue GitHooksService::PreReceiveError => ex
+ rescue Gitlab::Git::HooksService::PreReceiveError => ex
error(ex.message)
end
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 64b3c0118fb..1f059c31944 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -16,7 +16,7 @@ class DeleteBranchService < BaseService
else
error('Failed to remove branch')
end
- rescue GitHooksService::PreReceiveError => ex
+ rescue Gitlab::Git::HooksService::PreReceiveError => ex
error(ex.message)
end
diff --git a/app/services/discussions/update_diff_position_service.rb b/app/services/discussions/update_diff_position_service.rb
index 1ef8d9edbe1..746f209e20f 100644
--- a/app/services/discussions/update_diff_position_service.rb
+++ b/app/services/discussions/update_diff_position_service.rb
@@ -10,6 +10,10 @@ module Discussions
discussion.notes.each do |note|
if outdated
note.change_position = position
+
+ if project.resolve_outdated_diff_discussions?
+ note.resolve_without_save(current_user, resolved_by_push: true)
+ end
else
note.position = position
note.change_position = nil
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 0f3a485a3fd..0b7e4f187f7 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -71,7 +71,14 @@ class EventCreateService
end
def push(project, current_user, push_data)
- create_event(project, current_user, Event::PUSHED, data: push_data)
+ # 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)
+
+ PushEventPayloadService.new(event, push_data).execute
+ end
Users::ActivityService.new(current_user, 'push').execute
end
diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb
deleted file mode 100644
index eab65d09299..00000000000
--- a/app/services/git_hooks_service.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-class GitHooksService
- PreReceiveError = Class.new(StandardError)
-
- attr_accessor :oldrev, :newrev, :ref
-
- def execute(user, project, oldrev, newrev, ref)
- @project = project
- @user = Gitlab::GlId.gl_id(user)
- @oldrev = oldrev
- @newrev = newrev
- @ref = ref
-
- %w(pre-receive update).each do |hook_name|
- status, message = run_hook(hook_name)
-
- unless status
- raise PreReceiveError, message
- end
- end
-
- yield(self).tap do
- run_hook('post-receive')
- end
- end
-
- private
-
- def run_hook(name)
- hook = Gitlab::Git::Hook.new(name, @project)
- hook.trigger(@user, oldrev, newrev, ref)
- end
-end
diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb
deleted file mode 100644
index 545ca0742e4..00000000000
--- a/app/services/git_operation_service.rb
+++ /dev/null
@@ -1,157 +0,0 @@
-class GitOperationService
- attr_reader :user, :repository
-
- def initialize(new_user, new_repository)
- @user = new_user
- @repository = new_repository
- end
-
- def add_branch(branch_name, newrev)
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
- oldrev = Gitlab::Git::BLANK_SHA
-
- update_ref_in_hooks(ref, newrev, oldrev)
- end
-
- def rm_branch(branch)
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name
- oldrev = branch.target
- newrev = Gitlab::Git::BLANK_SHA
-
- update_ref_in_hooks(ref, newrev, oldrev)
- end
-
- def add_tag(tag_name, newrev, options = {})
- ref = Gitlab::Git::TAG_REF_PREFIX + tag_name
- oldrev = Gitlab::Git::BLANK_SHA
-
- with_hooks(ref, newrev, oldrev) do |service|
- # We want to pass the OID of the tag object to the hooks. For an
- # annotated tag we don't know that OID until after the tag object
- # (raw_tag) is created in the repository. That is why we have to
- # update the value after creating the tag object. Only the
- # "post-receive" hook will receive the correct value in this case.
- raw_tag = repository.rugged.tags.create(tag_name, newrev, options)
- service.newrev = raw_tag.target_id
- end
- end
-
- def rm_tag(tag)
- ref = Gitlab::Git::TAG_REF_PREFIX + tag.name
- oldrev = tag.target
- newrev = Gitlab::Git::BLANK_SHA
-
- update_ref_in_hooks(ref, newrev, oldrev) do
- repository.rugged.tags.delete(tag_name)
- end
- end
-
- # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist,
- # it would be created from `start_branch_name`.
- # If `start_project` is passed, and the branch doesn't exist,
- # it would try to find the commits from it instead of current repository.
- def with_branch(
- branch_name,
- start_branch_name: nil,
- start_project: repository.project,
- &block)
-
- start_repository = start_project.repository
- start_branch_name = nil if start_repository.empty_repo?
-
- if start_branch_name && !start_repository.branch_exists?(start_branch_name)
- raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.full_path}"
- end
-
- update_branch_with_hooks(branch_name) do
- repository.with_repo_branch_commit(
- start_repository,
- start_branch_name || branch_name,
- &block)
- end
- end
-
- private
-
- def update_branch_with_hooks(branch_name)
- update_autocrlf_option
-
- was_empty = repository.empty?
-
- # Make commit
- newrev = yield
-
- unless newrev
- raise Repository::CommitError.new('Failed to create commit')
- end
-
- branch = repository.find_branch(branch_name)
- oldrev = find_oldrev_from_branch(newrev, branch)
-
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
- update_ref_in_hooks(ref, newrev, oldrev)
-
- # If repo was empty expire cache
- repository.after_create if was_empty
- repository.after_create_branch if
- was_empty || Gitlab::Git.blank_ref?(oldrev)
-
- newrev
- end
-
- def find_oldrev_from_branch(newrev, branch)
- return Gitlab::Git::BLANK_SHA unless branch
-
- oldrev = branch.target
-
- if oldrev == repository.rugged.merge_base(newrev, branch.target)
- oldrev
- else
- raise Repository::CommitError.new('Branch diverged')
- end
- end
-
- def update_ref_in_hooks(ref, newrev, oldrev)
- with_hooks(ref, newrev, oldrev) do
- update_ref(ref, newrev, oldrev)
- end
- end
-
- def with_hooks(ref, newrev, oldrev)
- GitHooksService.new.execute(
- user,
- repository.project,
- oldrev,
- newrev,
- ref) do |service|
-
- yield(service)
- end
- end
-
- # Gitaly note: JV: wait with migrating #update_ref until we know how to migrate its call sites.
- def update_ref(ref, newrev, oldrev)
- # We use 'git update-ref' because libgit2/rugged currently does not
- # offer 'compare and swap' ref updates. Without compare-and-swap we can
- # (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(
- command,
- repository.path_to_repo) do |stdin|
- stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00")
- end
-
- unless status.zero?
- raise Repository::CommitError.new(
- "Could not update branch #{Gitlab::Git.branch_name(ref)}." \
- " Please refresh and try again.")
- end
- end
-
- def update_autocrlf_option
- if repository.raw_repository.autocrlf != :input
- repository.raw_repository.autocrlf = :input
- end
- end
-end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index ada2b64a3a6..bb61136e33b 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -30,7 +30,7 @@ class GitPushService < BaseService
@project.repository.after_create_branch
# Re-find the pushed commits.
- if is_default_branch?
+ if default_branch?
# Initial push to the default branch. Take the full history of that branch as "newly pushed".
process_default_branch
else
@@ -50,7 +50,7 @@ class GitPushService < BaseService
# Update the bare repositories info/attributes file using the contents of the default branches
# .gitattributes file
- update_gitattributes if is_default_branch?
+ update_gitattributes if default_branch?
end
execute_related_hooks
@@ -66,7 +66,7 @@ class GitPushService < BaseService
end
def update_caches
- if is_default_branch?
+ if default_branch?
if push_to_new_branch?
# If this is the initial push into the default branch, the file type caches
# will already be reset as a result of `Project#change_head`.
@@ -90,14 +90,25 @@ class GitPushService < BaseService
end
def update_signatures
- @push_commits.each do |commit|
- CreateGpgSignatureWorker.perform_async(commit.sha, @project.id)
+ commit_shas = @push_commits.last(PROCESS_COMMIT_LIMIT).map(&:sha)
+
+ return if commit_shas.empty?
+
+ shas_with_cached_signatures = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha)
+ commit_shas -= shas_with_cached_signatures
+
+ return if commit_shas.empty?
+
+ commit_shas = Gitlab::Git::Commit.shas_with_signatures(project.repository, commit_shas)
+
+ commit_shas.each do |sha|
+ CreateGpgSignatureWorker.perform_async(sha, project.id)
end
end
# Schedules processing of commit messages.
def process_commit_messages
- default = is_default_branch?
+ default = default_branch?
@push_commits.last(PROCESS_COMMIT_LIMIT).each do |commit|
if commit.matches_cross_reference_regex?
@@ -191,7 +202,7 @@ class GitPushService < BaseService
Gitlab::Git.branch_ref?(params[:ref])
end
- def is_default_branch?
+ def default_branch?
Gitlab::Git.branch_ref?(params[:ref]) &&
(Gitlab::Git.ref_name(params[:ref]) == project.default_branch || project.default_branch.nil?)
end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index c4e9b8fd8e0..70e50aa0f12 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -8,15 +8,7 @@ module Groups
def execute
@group = Group.new(params)
- unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
- deny_visibility_level(@group)
- return @group
- end
-
- if @group.parent && !can?(current_user, :admin_group, @group.parent)
- @group.parent = nil
- @group.errors.add(:parent_id, 'manage access required to create subgroup')
-
+ unless can_use_visibility_level? && can_create_group?
return @group
end
@@ -39,5 +31,33 @@ module Groups
def create_chat_team?
Gitlab.config.mattermost.enabled && @chat_team && group.chat_team.nil?
end
+
+ def can_create_group?
+ if @group.subgroup?
+ unless can?(current_user, :create_subgroup, @group.parent)
+ @group.parent = nil
+ @group.errors.add(:parent_id, 'You don’t have permission to create a subgroup in this group.')
+
+ return false
+ end
+ else
+ unless can?(current_user, :create_group)
+ @group.errors.add(:base, 'You don’t have permission to create groups.')
+
+ return false
+ end
+ end
+
+ true
+ end
+
+ def can_use_visibility_level?
+ unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
+ deny_visibility_level(@group)
+ return false
+ end
+
+ true
+ end
end
end
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
index f565612a89d..e3f9d9ee95d 100644
--- a/app/services/groups/destroy_service.rb
+++ b/app/services/groups/destroy_service.rb
@@ -13,7 +13,7 @@ module Groups
# Execute the destruction of the models immediately to ensure atomic cleanup.
# Skip repository removal because we remove directory with namespace
# that contain all these repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
+ ::Projects::DestroyService.new(project, current_user, skip_repo: project.legacy_storage?).execute
end
group.children.each do |group|
diff --git a/app/services/groups/nested_create_service.rb b/app/services/groups/nested_create_service.rb
new file mode 100644
index 00000000000..d6f08fc3cce
--- /dev/null
+++ b/app/services/groups/nested_create_service.rb
@@ -0,0 +1,49 @@
+module Groups
+ class NestedCreateService < Groups::BaseService
+ attr_reader :group_path
+
+ def initialize(user, params)
+ @current_user, @params = user, params.dup
+
+ @group_path = @params.delete(:group_path)
+ end
+
+ def execute
+ return nil unless group_path
+
+ if group = Group.find_by_full_path(group_path)
+ return group
+ end
+
+ if group_path.include?('/') && !Group.supports_nested_groups?
+ raise 'Nested groups are not supported on MySQL'
+ end
+
+ create_group_path
+ end
+
+ private
+
+ def create_group_path
+ group_path_segments = group_path.split('/')
+
+ last_group = nil
+ partial_path_segments = []
+ while subgroup_name = group_path_segments.shift
+ partial_path_segments << subgroup_name
+ partial_path = partial_path_segments.join('/')
+
+ new_params = params.reverse_merge(
+ path: subgroup_name,
+ name: subgroup_name,
+ parent: last_group
+ )
+ new_params[:visibility_level] ||= Gitlab::CurrentSettings.current_application_settings.default_group_visibility
+
+ last_group = Group.find_by_full_path(partial_path) || Groups::CreateService.new(current_user, new_params).execute
+ end
+
+ last_group
+ end
+ end
+end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 1d65c76d282..08e3efb96e3 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -1,18 +1,13 @@
module Groups
class UpdateService < Groups::BaseService
+ include UpdateVisibilityLevel
+
def execute
reject_parent_id!
- # check that user is allowed to set specified visibility_level
- new_visibility = params[:visibility_level]
- if new_visibility && new_visibility.to_i != group.visibility_level
- unless can?(current_user, :change_visibility_level, group) &&
- Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+ return false unless valid_visibility_level_change?(group, params[:visibility_level])
- deny_visibility_level(group, new_visibility)
- return group
- end
- end
+ return false unless valid_share_with_group_lock_change?
group.assign_attributes(params)
@@ -30,5 +25,19 @@ module Groups
def reject_parent_id!
params.except!(:parent_id)
end
+
+ def valid_share_with_group_lock_change?
+ return true unless changing_share_with_group_lock?
+ return true if can?(current_user, :change_share_with_group_lock, group)
+
+ group.errors.add(:share_with_group_lock, s_('GroupSettings|cannot be disabled when the parent group "Share with group lock" is enabled, except by the owner of the parent group'))
+ false
+ end
+
+ def changing_share_with_group_lock?
+ return false if params[:share_with_group_lock].nil?
+
+ params[:share_with_group_lock] != group.share_with_group_lock
+ end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index b84a6fd2b7d..8b967b78052 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -56,6 +56,7 @@ class IssuableBaseService < BaseService
params.delete(:assignee_id)
params.delete(:due_date)
params.delete(:canonical_issue_id)
+ params.delete(:project)
end
filter_assignee(issuable)
@@ -192,6 +193,8 @@ class IssuableBaseService < BaseService
def after_create(issuable)
# To be overridden by subclasses
+
+ issuable.update_project_counter_caches
end
def before_update(issuable)
@@ -200,6 +203,8 @@ class IssuableBaseService < BaseService
def after_update(issuable)
# To be overridden by subclasses
+
+ issuable.update_project_counter_caches
end
def update(issuable)
@@ -240,9 +245,7 @@ class IssuableBaseService < BaseService
new_assignees = issuable.assignees.to_a
affected_assignees = (old_assignees + new_assignees) - (old_assignees & new_assignees)
- # Don't clear the project cache, because it will be handled by the
- # appropriate service (close / reopen / merge / etc.).
- invalidate_cache_counts(issuable, users: affected_assignees.compact, skip_project_cache: true)
+ invalidate_cache_counts(issuable, users: affected_assignees.compact)
after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
@@ -336,18 +339,9 @@ class IssuableBaseService < BaseService
create_labels_note(issuable, old_labels) if issuable.labels != old_labels
end
- def invalidate_cache_counts(issuable, users: [], skip_project_cache: false)
+ def invalidate_cache_counts(issuable, users: [])
users.each do |user|
- user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts")
- end
-
- unless skip_project_cache
- case issuable
- when Issue
- IssuesFinder.new(nil, project_id: issuable.project_id).clear_caches!
- when MergeRequest
- MergeRequestsFinder.new(nil, project_id: issuable.target_project_id).clear_caches!
- end
+ user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 9114f0ccc81..0307634c0b6 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -16,6 +16,7 @@ module Issues
spam_check(issue, current_user)
issue.move_to_end
+ # current_user (defined in BaseService) is not available within run_after_commit block
user = current_user
issue.run_after_commit do
NewIssueWorker.perform_async(issue.id, user.id)
@@ -26,6 +27,8 @@ module Issues
todo_service.new_issue(issuable, current_user)
user_agent_detail_service.create
resolve_discussions_with_issue(issuable)
+
+ super
end
def resolve_discussions_with_issue(issue)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 8d918ccc635..b4ca3966505 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -3,10 +3,10 @@ module Issues
include SpamCheckService
def execute(issue)
- handle_move_between_iids(issue)
+ handle_move_between_ids(issue)
filter_spam_check_params
change_issue_duplicate(issue)
- update(issue)
+ move_issue_to_new_project(issue) || update(issue)
end
def before_update(issue)
@@ -54,13 +54,13 @@ module Issues
end
end
- def handle_move_between_iids(issue)
- return unless params[:move_between_iids]
+ def handle_move_between_ids(issue)
+ return unless params[:move_between_ids]
- after_iid, before_iid = params.delete(:move_between_iids)
+ after_id, before_id = params.delete(:move_between_ids)
- issue_before = get_issue_if_allowed(issue.project, before_iid) if before_iid
- issue_after = get_issue_if_allowed(issue.project, after_iid) if after_iid
+ issue_before = get_issue_if_allowed(issue.project, before_id) if before_id
+ issue_after = get_issue_if_allowed(issue.project, after_id) if after_id
issue.move_between(issue_before, issue_after)
end
@@ -74,10 +74,21 @@ module Issues
end
end
+ def move_issue_to_new_project(issue)
+ target_project = params.delete(:target_project)
+
+ return unless target_project &&
+ issue.can_move?(current_user, target_project) &&
+ target_project != issue.project
+
+ update(issue)
+ Issues::MoveService.new(project, current_user).execute(issue, target_project)
+ end
+
private
- def get_issue_if_allowed(project, iid)
- issue = project.issues.find_by(iid: iid)
+ def get_issue_if_allowed(project, id)
+ issue = project.issues.find(id)
issue if can?(current_user, :update_issue, issue)
end
diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb
index d2ece354efc..775efed48eb 100644
--- a/app/services/labels/transfer_service.rb
+++ b/app/services/labels/transfer_service.rb
@@ -37,7 +37,7 @@ module Labels
union = Gitlab::SQL::Union.new(label_ids)
- Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq
+ Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq # rubocop:disable GitlabSecurity/SqlInjection
end
def group_labels_applied_to_issues
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 2e089149ca8..46c505baf8b 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -31,7 +31,7 @@ module Members
source.members.find_by(condition) ||
source.requesters.find_by!(condition)
else
- source.public_send(scope).find_by!(condition)
+ source.public_send(scope).find_by!(condition) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb
index 738cedbaed7..da39a380451 100644
--- a/app/services/merge_requests/create_from_issue_service.rb
+++ b/app/services/merge_requests/create_from_issue_service.rb
@@ -3,6 +3,8 @@ module MergeRequests
def execute
return error('Invalid issue iid') unless issue_iid.present? && issue.present?
+ params[:label_ids] = issue.label_ids if issue.label_ids.any?
+
result = CreateBranchService.new(project, current_user).execute(branch_name, ref)
return result if result[:status] == :error
@@ -43,7 +45,8 @@ module MergeRequests
{
source_project_id: project.id,
source_branch: branch_name,
- target_project_id: project.id
+ target_project_id: project.id,
+ milestone_id: issue.milestone_id
}
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 7d539fa49e6..3d53fe0646b 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -17,6 +17,7 @@ module MergeRequests
end
def before_create(merge_request)
+ # current_user (defined in BaseService) is not available within run_after_commit block
user = current_user
merge_request.run_after_commit do
NewMergeRequestWorker.perform_async(merge_request.id, user.id)
@@ -24,10 +25,11 @@ module MergeRequests
end
def after_create(issuable)
- event_service.open_mr(issuable, current_user)
todo_service.new_merge_request(issuable, current_user)
issuable.cache_merge_request_closes_issues!(current_user)
update_merge_requests_head_pipeline(issuable)
+
+ super
end
private
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index bc846e07f24..b2b6c5627fb 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -26,10 +26,12 @@ module MergeRequests
merge_request.in_locked_state do
if commit
after_merge
+ clean_merge_jid
success
end
end
rescue MergeError => e
+ clean_merge_jid
log_merge_error(e.message, save_message_on_model: true)
end
@@ -49,7 +51,7 @@ module MergeRequests
raise MergeError, 'Conflicts detected during merge' unless commit_id
merge_request.update(merge_commit_sha: commit_id)
- rescue GitHooksService::PreReceiveError => e
+ rescue Gitlab::Git::HooksService::PreReceiveError => e
raise MergeError, e.message
rescue StandardError => e
raise MergeError, "Something went wrong during merge: #{e.message}"
@@ -70,6 +72,10 @@ module MergeRequests
end
end
+ def clean_merge_jid
+ merge_request.update_column(:merge_jid, nil)
+ end
+
def branch_deletion_user
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
diff --git a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
index aed5287940e..850deb0ac7a 100644
--- a/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
+++ b/app/services/merge_requests/merge_when_pipeline_succeeds_service.rb
@@ -30,7 +30,7 @@ module MergeRequests
next
end
- MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
+ merge_request.merge_async(merge_request.merge_user_id, merge_request.merge_params)
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 75a65aecd1a..2832d893e95 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -83,7 +83,7 @@ module MergeRequests
if merge_request.head_pipeline && merge_request.head_pipeline.active?
MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request)
else
- MergeWorker.perform_async(merge_request.id, current_user.id, {})
+ merge_request.merge_async(current_user.id, {})
end
end
diff --git a/app/services/milestones/close_service.rb b/app/services/milestones/close_service.rb
index 776ec4b287b..5b06c4b601d 100644
--- a/app/services/milestones/close_service.rb
+++ b/app/services/milestones/close_service.rb
@@ -1,7 +1,7 @@
module Milestones
class CloseService < Milestones::BaseService
def execute(milestone)
- if milestone.close && milestone.is_project_milestone?
+ if milestone.close && milestone.project_milestone?
event_service.close_milestone(milestone, current_user)
end
diff --git a/app/services/milestones/create_service.rb b/app/services/milestones/create_service.rb
index aef3124c7e3..ed2e833d833 100644
--- a/app/services/milestones/create_service.rb
+++ b/app/services/milestones/create_service.rb
@@ -3,7 +3,7 @@ module Milestones
def execute
milestone = parent.milestones.new(params)
- if milestone.save && milestone.is_project_milestone?
+ if milestone.save && milestone.project_milestone?
event_service.open_milestone(milestone, current_user)
end
diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb
index 600ebcfbecb..b18651476a8 100644
--- a/app/services/milestones/destroy_service.rb
+++ b/app/services/milestones/destroy_service.rb
@@ -1,7 +1,7 @@
module Milestones
class DestroyService < Milestones::BaseService
def execute(milestone)
- return unless milestone.is_project_milestone?
+ return unless milestone.project_milestone?
Milestone.transaction do
update_params = { milestone: nil }
diff --git a/app/services/milestones/reopen_service.rb b/app/services/milestones/reopen_service.rb
index 5b8b682caaf..3efb33157c5 100644
--- a/app/services/milestones/reopen_service.rb
+++ b/app/services/milestones/reopen_service.rb
@@ -1,7 +1,7 @@
module Milestones
class ReopenService < Milestones::BaseService
def execute(milestone)
- if milestone.activate && milestone.is_project_milestone?
+ if milestone.activate && milestone.project_milestone?
event_service.reopen_milestone(milestone, current_user)
end
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 21c9c314a2a..c9f07c140f7 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -95,7 +95,7 @@ module NotificationRecipientService
def add_participants(user)
return unless target.respond_to?(:participants)
- self << [target.participants(user), :watch]
+ self << [target.participants(user), :participating]
end
# Get project/group users with CUSTOM notification level
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index df04b1a4fe3..e2a80db06a6 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -1,3 +1,5 @@
+# rubocop:disable GitlabSecurity/PublicSend
+
# NotificationService class
#
# Used for notifying users with emails about different events
@@ -10,9 +12,11 @@ class NotificationService
# only if ssh key is not deploy key
#
# This is security email so it will be sent
- # even if user disabled notifications
+ # even if user disabled notifications. However,
+ # it won't be sent to internal users like the
+ # ghost user or the EE support bot.
def new_key(key)
- if key.user
+ if key.user&.can?(:receive_notifications)
mailer.new_ssh_key_email(key.id).deliver_later
end
end
@@ -22,14 +26,14 @@ class NotificationService
# This is a security email so it will be sent even if the user user disabled
# notifications
def new_gpg_key(gpg_key)
- if gpg_key.user
+ if gpg_key.user&.can?(:receive_notifications)
mailer.new_gpg_key_email(gpg_key.id).deliver_later
end
end
# Always notify user about email added to profile
def new_email(email)
- if email.user
+ if email.user&.can?(:receive_notifications)
mailer.new_email_email(email.id).deliver_later
end
end
@@ -185,6 +189,8 @@ class NotificationService
# Notify new user with email after creation
def new_user(user, token = nil)
+ return true unless notifiable?(user, :mention)
+
# Don't email omniauth created users
mailer.new_user_email(user.id, token).deliver_later unless user.identities.any?
end
@@ -206,19 +212,27 @@ class NotificationService
# Members
def new_access_request(member)
+ return true unless member.notifiable?(:subscription)
+
mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later
end
def decline_access_request(member)
+ return true unless member.notifiable?(:subscription)
+
mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later
end
# Project invite
def invite_project_member(project_member, token)
+ return true unless project_member.notifiable?(:subscription)
+
mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later
end
def accept_project_invite(project_member)
+ return true unless project_member.notifiable?(:subscription)
+
mailer.member_invite_accepted_email(project_member.real_source_type, project_member.id).deliver_later
end
@@ -232,10 +246,14 @@ class NotificationService
end
def new_project_member(project_member)
+ return true unless project_member.notifiable?(:mention, skip_read_ability: true)
+
mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end
def update_project_member(project_member)
+ return true unless project_member.notifiable?(:mention)
+
mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end
@@ -249,6 +267,9 @@ class NotificationService
end
def decline_group_invite(group_member)
+ # always send this one, since it's a response to the user's own
+ # action
+
mailer.member_invite_declined_email(
group_member.real_source_type,
group_member.group.id,
@@ -258,15 +279,19 @@ class NotificationService
end
def new_group_member(group_member)
+ return true unless group_member.notifiable?(:mention)
+
mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end
def update_group_member(group_member)
+ return true unless group_member.notifiable?(:mention)
+
mailer.member_access_granted_email(group_member.real_source_type, group_member.id).deliver_later
end
def project_was_moved(project, old_path_with_namespace)
- recipients = NotificationRecipientService.notifiable_users(project.team.members, :mention, project: project)
+ recipients = notifiable_users(project.team.members, :mention, project: project)
recipients.each do |recipient|
mailer.project_was_moved_email(
@@ -288,10 +313,14 @@ class NotificationService
end
def project_exported(project, current_user)
+ return true unless notifiable?(current_user, :mention, project: project)
+
mailer.project_was_exported_email(current_user, project).deliver_later
end
def project_not_exported(project, current_user, errors)
+ return true unless notifiable?(current_user, :mention, project: project)
+
mailer.project_was_not_exported_email(current_user, project, errors).deliver_later
end
@@ -300,7 +329,7 @@ class NotificationService
return unless mailer.respond_to?(email_template)
- recipients ||= NotificationRecipientService.notifiable_users(
+ recipients ||= notifiable_users(
[pipeline.user], :watch,
custom_action: :"#{pipeline.status}_pipeline",
target: pipeline
@@ -369,7 +398,7 @@ class NotificationService
def relabeled_resource_email(target, labels, current_user, method)
recipients = labels.flat_map { |l| l.subscribers(target.project) }
- recipients = NotificationRecipientService.notifiable_users(
+ recipients = notifiable_users(
recipients, :subscription,
target: target,
acting_user: current_user
@@ -401,4 +430,14 @@ class NotificationService
object.previous_changes[attribute].first
end
end
+
+ private
+
+ def notifiable?(*args)
+ NotificationRecipientService.notifiable?(*args)
+ end
+
+ def notifiable_users(*args)
+ NotificationRecipientService.notifiable_users(*args)
+ end
end
diff --git a/app/services/projects/after_import_service.rb b/app/services/projects/after_import_service.rb
new file mode 100644
index 00000000000..3047268b2d1
--- /dev/null
+++ b/app/services/projects/after_import_service.rb
@@ -0,0 +1,24 @@
+module Projects
+ class AfterImportService
+ RESERVED_REF_PREFIXES = Repository::RESERVED_REFS_NAMES.map { |n| File.join('refs', n, '/') }
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ Projects::HousekeepingService.new(@project).execute do
+ repository.delete_all_refs_except(RESERVED_REF_PREFIXES)
+ end
+ rescue Projects::HousekeepingService::LeaseTaken => e
+ Rails.logger.info(
+ "Could not perform housekeeping for project #{@project.full_path} (#{@project.id}): #{e}")
+ end
+
+ private
+
+ def repository
+ @repository ||= @project.repository
+ end
+ end
+end
diff --git a/app/services/projects/count_service.rb b/app/services/projects/count_service.rb
new file mode 100644
index 00000000000..5e633c37bf8
--- /dev/null
+++ b/app/services/projects/count_service.rb
@@ -0,0 +1,43 @@
+module Projects
+ # Base class for the various service classes that count project data (e.g.
+ # issues or forks).
+ class CountService
+ def initialize(project)
+ @project = project
+ end
+
+ def relation_for_count
+ raise(
+ NotImplementedError,
+ '"relation_for_count" must be implemented and return an ActiveRecord::Relation'
+ )
+ end
+
+ def count
+ Rails.cache.fetch(cache_key) { uncached_count }
+ end
+
+ def refresh_cache
+ Rails.cache.write(cache_key, uncached_count)
+ end
+
+ def uncached_count
+ relation_for_count.count
+ end
+
+ def delete_cache
+ Rails.cache.delete(cache_key)
+ end
+
+ def cache_key_name
+ raise(
+ NotImplementedError,
+ '"cache_key_name" must be implemented and return a String'
+ )
+ end
+
+ def cache_key
+ ['projects', @project.id, cache_key_name]
+ end
+ end
+end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 48578b6d9e5..71533da31b1 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -44,6 +44,8 @@ module Projects
@project.namespace_id = current_user.namespace_id
end
+ yield(@project) if block_given?
+
@project.creator = current_user
if forked_from_project_id
@@ -99,12 +101,19 @@ module Projects
event_service.create_project(@project, current_user)
system_hook_service.execute_hooks_for(@project, :create)
- unless @project.group || @project.gitlab_project_import?
- owners = [current_user, @project.namespace.owner].compact.uniq
- @project.add_master(owners, current_user: current_user)
- end
+ setup_authorizations
+ end
- @project.group&.refresh_members_authorized_projects
+ # Refresh the current user's authorizations inline (so they can access the
+ # project immediately after this request completes), and any other affected
+ # users in the background
+ def setup_authorizations
+ if @project.group
+ @project.group.refresh_members_authorized_projects(blocking: false)
+ current_user.refresh_authorized_projects
+ else
+ @project.add_master(@project.namespace.owner, current_user: current_user)
+ end
end
def skip_wiki?
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 11ad4838471..54eb75ab9bf 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -128,6 +128,8 @@ module Projects
project.repository.before_delete
Repository.new(wiki_path, project, disk_path: repo_path).before_delete
+
+ Projects::ForksCountService.new(project).delete_cache
end
end
end
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index a2b23ea6171..ad67e68a86a 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -21,11 +21,17 @@ module Projects
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
+ refresh_forks_count
+
new_project
end
private
+ def refresh_forks_count
+ Projects::ForksCountService.new(@project).refresh_cache
+ end
+
def allowed_visibility_level
project_level = @project.visibility_level
diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb
new file mode 100644
index 00000000000..3a0fa84b868
--- /dev/null
+++ b/app/services/projects/forks_count_service.rb
@@ -0,0 +1,12 @@
+module Projects
+ # Service class for getting and caching the number of forks of a project.
+ class ForksCountService < CountService
+ def relation_for_count
+ @project.forks
+ end
+
+ def cache_key_name
+ 'forks_count'
+ end
+ end
+end
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index 4b8946f8ee2..dcef8b66215 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -9,7 +9,8 @@ module Projects
class HousekeepingService < BaseService
include Gitlab::CurrentSettings
- LEASE_TIMEOUT = 3600
+ # Timeout set to 24h
+ LEASE_TIMEOUT = 86400
class LeaseTaken < StandardError
def to_s
@@ -25,6 +26,8 @@ module Projects
lease_uuid = try_obtain_lease
raise LeaseTaken unless lease_uuid.present?
+ yield if block_given?
+
execute_gitlab_shell_gc(lease_uuid)
end
diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb
new file mode 100644
index 00000000000..3c0d186a73c
--- /dev/null
+++ b/app/services/projects/open_issues_count_service.rb
@@ -0,0 +1,15 @@
+module Projects
+ # Service class for counting and caching the number of open issues of a
+ # project.
+ class OpenIssuesCountService < CountService
+ def relation_for_count
+ # We don't include confidential issues in this number since this would
+ # expose the number of confidential issues to non project members.
+ @project.issues.opened.public_only
+ end
+
+ def cache_key_name
+ 'open_issues_count'
+ end
+ end
+end
diff --git a/app/services/projects/open_merge_requests_count_service.rb b/app/services/projects/open_merge_requests_count_service.rb
new file mode 100644
index 00000000000..2a90f78b90d
--- /dev/null
+++ b/app/services/projects/open_merge_requests_count_service.rb
@@ -0,0 +1,13 @@
+module Projects
+ # Service class for counting and caching the number of open merge requests of
+ # a project.
+ class OpenMergeRequestsCountService < CountService
+ def relation_for_count
+ @project.merge_requests.opened
+ end
+
+ def cache_key_name
+ 'open_merge_requests_count'
+ end
+ end
+end
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index f385e426827..f30b40423c8 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -13,7 +13,13 @@ module Projects
::MergeRequests::CloseService.new(@project, @current_user).execute(mr)
end
+ refresh_forks_count(@project.forked_from_project)
+
@project.forked_project_link.destroy
end
+
+ def refresh_forks_count(project)
+ Projects::ForksCountService.new(project).refresh_cache
+ end
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index f27e90faaab..78cf195d3b3 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -1,5 +1,7 @@
module Projects
class UpdatePagesService < BaseService
+ include Gitlab::CurrentSettings
+
BLOCK_SIZE = 32.kilobytes
MAX_SIZE = 1.terabyte
SITE_PATH = 'public/'.freeze
@@ -51,7 +53,7 @@ module Projects
log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest?
@status.description = message
- @status.drop
+ @status.drop(:script_failure)
super
end
@@ -176,11 +178,11 @@ module Projects
end
def register_attempt
- pages_deployments_total_counter.increase
+ pages_deployments_total_counter.increment
end
def register_failure
- pages_deployments_failed_total_counter.increase
+ pages_deployments_failed_total_counter.increment
end
def pages_deployments_total_counter
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index d81035e4eba..cb4ffcab778 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -1,7 +1,9 @@
module Projects
class UpdateService < BaseService
+ include UpdateVisibilityLevel
+
def execute
- unless visibility_level_allowed?
+ unless valid_visibility_level_change?(project, params[:visibility_level])
return error('New visibility level not allowed!')
end
@@ -10,7 +12,7 @@ module Projects
end
if changing_default_branch?
- project.change_head(params[:default_branch])
+ return error("Could not set the default branch") unless project.change_head(params[:default_branch])
end
if project.update_attributes(params.except(:default_branch))
@@ -28,22 +30,6 @@ module Projects
private
- def visibility_level_allowed?
- # check that user is allowed to set specified visibility_level
- new_visibility = params[:visibility_level]
-
- if new_visibility && new_visibility.to_i != project.visibility_level
- unless can?(current_user, :change_visibility_level, project) &&
- Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
-
- deny_visibility_level(project, new_visibility)
- return false
- end
- end
-
- true
- end
-
def renaming_project_with_container_registry_tags?
new_path = params[:path]
diff --git a/app/services/push_event_payload_service.rb b/app/services/push_event_payload_service.rb
new file mode 100644
index 00000000000..b0a389c85f9
--- /dev/null
+++ b/app/services/push_event_payload_service.rb
@@ -0,0 +1,120 @@
+# Service class for creating push event payloads as stored in the
+# "push_event_payloads" table.
+#
+# Example:
+#
+# data = Gitlab::DataBuilder::Push.build(...)
+# event = Event.create(...)
+#
+# PushEventPayloadService.new(event, data).execute
+class PushEventPayloadService
+ # event - The event this push payload belongs to.
+ # push_data - A Hash produced by `Gitlab::DataBuilder::Push.build` to use for
+ # building the push payload.
+ def initialize(event, push_data)
+ @event = event
+ @push_data = push_data
+ end
+
+ # Creates and returns a new PushEventPayload row.
+ #
+ # This method will raise upon encountering validation errors.
+ #
+ # Returns an instance of PushEventPayload.
+ def execute
+ @event.build_push_event_payload(
+ commit_count: commit_count,
+ action: action,
+ ref_type: ref_type,
+ commit_from: commit_from_id,
+ commit_to: commit_to_id,
+ ref: trimmed_ref,
+ commit_title: commit_title,
+ event_id: @event.id
+ )
+
+ @event.push_event_payload.save!
+ @event.push_event_payload
+ end
+
+ # Returns the commit title to use.
+ #
+ # The commit title is limited to the first line and a maximum of 70
+ # characters.
+ def commit_title
+ commit = @push_data.fetch(:commits).last
+
+ return nil unless commit && commit[:message]
+
+ raw_msg = commit[:message]
+
+ # Find where the first line ends, without turning the entire message into an
+ # Array of lines (this is a waste of memory for large commit messages).
+ index = raw_msg.index("\n")
+ message = index ? raw_msg[0..index] : raw_msg
+
+ message.strip.truncate(70)
+ end
+
+ def commit_from_id
+ if create?
+ nil
+ else
+ revision_before
+ end
+ end
+
+ def commit_to_id
+ if remove?
+ nil
+ else
+ revision_after
+ end
+ end
+
+ def commit_count
+ @push_data.fetch(:total_commits_count)
+ end
+
+ def ref
+ @push_data.fetch(:ref)
+ end
+
+ def revision_before
+ @push_data.fetch(:before)
+ end
+
+ def revision_after
+ @push_data.fetch(:after)
+ end
+
+ def trimmed_ref
+ Gitlab::Git.ref_name(ref)
+ end
+
+ def create?
+ Gitlab::Git.blank_ref?(revision_before)
+ end
+
+ def remove?
+ Gitlab::Git.blank_ref?(revision_after)
+ end
+
+ def action
+ if create?
+ :created
+ elsif remove?
+ :removed
+ else
+ :pushed
+ end
+ end
+
+ def ref_type
+ if Gitlab::Git.tag_ref?(ref)
+ :tag
+ else
+ :branch
+ end
+ end
+end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index c7832c47e1a..a077b3584b0 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -115,7 +115,7 @@ module QuickActions
if issuable.allows_multiple_assignees?
issuable.assignees.pluck(:id) + users.map(&:id)
else
- [users.last.id]
+ [users.first.id]
end
end
@@ -505,6 +505,24 @@ module QuickActions
end
end
+ desc 'Move this issue to another project.'
+ explanation do |path_to_project|
+ "Moves this issue to #{path_to_project}."
+ end
+ params 'path/to/project'
+ condition do
+ issuable.is_a?(Issue) &&
+ issuable.persisted? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :move do |target_project_path|
+ target_project = Project.find_by_full_path(target_project_path)
+
+ if target_project.present?
+ @updates[:target_project] = target_project
+ end
+ end
+
def extract_users(params)
return [] if params.nil?
diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb
index 3e65b7d31a3..73ea3018fbd 100644
--- a/app/services/spam_service.rb
+++ b/app/services/spam_service.rb
@@ -45,7 +45,7 @@ class SpamService
def check(api)
return false unless request && check_for_spam?
- return false unless akismet.is_spam?
+ return false unless akismet.spam?
create_spam_log(api)
true
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index cbcd4478af6..a1c2f8d0180 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -4,7 +4,7 @@ class SystemHooksService
end
def execute_hooks(data, hooks_scope = :all)
- SystemHook.public_send(hooks_scope).find_each do |hook|
+ SystemHook.public_send(hooks_scope).find_each do |hook| # rubocop:disable GitlabSecurity/PublicSend
hook.async_execute(data, 'system_hooks')
end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 1763f64a4e4..1f66a2668f9 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -142,7 +142,7 @@ module SystemNoteService
#
# Returns the created Note object
def change_milestone(noteable, project, author, milestone)
- format = milestone&.is_group_milestone? ? :name : :iid
+ format = milestone&.group_milestone? ? :name : :iid
body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb
index 674792f6138..b3f4a72d6fe 100644
--- a/app/services/tags/create_service.rb
+++ b/app/services/tags/create_service.rb
@@ -13,7 +13,7 @@ module Tags
new_tag = repository.add_tag(current_user, tag_name, target, message)
rescue Rugged::TagError
return error("Tag #{tag_name} already exists")
- rescue GitHooksService::PreReceiveError => ex
+ rescue Gitlab::Git::HooksService::PreReceiveError => ex
return error(ex.message)
end
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index a368f4f5b61..d3d46064729 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -21,7 +21,7 @@ module Tags
else
error('Failed to remove tag')
end
- rescue GitHooksService::PreReceiveError => ex
+ rescue Gitlab::Git::HooksService::PreReceiveError => ex
error(ex.message)
end
diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb
index 74ba814afff..20d90504bd2 100644
--- a/app/services/test_hooks/base_service.rb
+++ b/app/services/test_hooks/base_service.rb
@@ -9,18 +9,17 @@ module TestHooks
end
def execute
+ trigger_key = hook.class::TRIGGERS.key(trigger.to_sym)
trigger_data_method = "#{trigger}_data"
- if !self.respond_to?(trigger_data_method, true) ||
- !hook.class::TRIGGERS.value?(trigger.to_sym)
-
+ if trigger_key.nil? || !self.respond_to?(trigger_data_method, true)
return error('Testing not available for this hook')
end
error_message = catch(:validation_error) do
- sample_data = self.__send__(trigger_data_method)
+ sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend
- return hook.execute(sample_data, trigger)
+ return hook.execute(sample_data, trigger_key)
end
error(error_message)
diff --git a/app/services/test_hooks/system_service.rb b/app/services/test_hooks/system_service.rb
index 76c3c19bd74..67552edefc9 100644
--- a/app/services/test_hooks/system_service.rb
+++ b/app/services/test_hooks/system_service.rb
@@ -2,47 +2,16 @@ module TestHooks
class SystemService < TestHooks::BaseService
private
- def project
- @project ||= begin
- project = Project.first
-
- throw(:validation_error, 'Ensure that at least one project exists.') unless project
-
- project
- end
- end
-
def push_events_data
- if project.empty_repo?
- throw(:validation_error, "Ensure project \"#{project.human_name}\" has commits.")
- end
-
- Gitlab::DataBuilder::Push.build_sample(project, current_user)
+ Gitlab::DataBuilder::Push.sample_data
end
def tag_push_events_data
- if project.repository.tags.empty?
- throw(:validation_error, "Ensure project \"#{project.human_name}\" has tags.")
- end
-
- Gitlab::DataBuilder::Push.build_sample(project, current_user)
+ Gitlab::DataBuilder::Push.sample_data
end
def repository_update_events_data
- commit = project.commit
- ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
-
- unless commit
- throw(:validation_error, "Ensure project \"#{project.human_name}\" has commits.")
- end
-
- change = Gitlab::DataBuilder::Repository.single_change(
- commit.parent_id || Gitlab::Git::BLANK_SHA,
- commit.id,
- ref
- )
-
- Gitlab::DataBuilder::Repository.update(project, current_user, [change], [ref])
+ Gitlab::DataBuilder::Repository.sample_data
end
end
end
diff --git a/app/services/upload_service.rb b/app/services/upload_service.rb
index 6c5b2baff41..76700dfcdee 100644
--- a/app/services/upload_service.rb
+++ b/app/services/upload_service.rb
@@ -1,4 +1,6 @@
class UploadService
+ include Gitlab::CurrentSettings
+
def initialize(model, file, uploader_class = FileUploader)
@model, @file, @uploader_class = model, file, uploader_class
end
diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb
index d7a6804ee88..8630e572624 100644
--- a/app/services/user_project_access_changed_service.rb
+++ b/app/services/user_project_access_changed_service.rb
@@ -3,7 +3,13 @@ class UserProjectAccessChangedService
@user_ids = Array.wrap(user_ids)
end
- def execute
- AuthorizedProjectsWorker.bulk_perform_and_wait(@user_ids.map { |id| [id] })
+ def execute(blocking: true)
+ bulk_args = @user_ids.map { |id| [id] }
+
+ if blocking
+ AuthorizedProjectsWorker.bulk_perform_and_wait(bulk_args)
+ else
+ AuthorizedProjectsWorker.bulk_perform_async(bulk_args)
+ end
end
end
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index ff234a3440f..6f05500adea 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -1,5 +1,7 @@
module Users
class BuildService < BaseService
+ include Gitlab::CurrentSettings
+
def initialize(current_user, params = {})
@current_user = current_user
@params = params.dup
diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb
index 74abc017cea..c8a3c461d60 100644
--- a/app/services/users/create_service.rb
+++ b/app/services/users/create_service.rb
@@ -1,5 +1,7 @@
module Users
class CreateService < BaseService
+ include NewUserNotifier
+
def initialize(current_user, params = {})
@current_user = current_user
@params = params.dup
@@ -10,11 +12,7 @@ module Users
@reset_token = user.generate_reset_token if user.recently_sent_password_reset?
- if user.save
- log_info("User \"#{user.name}\" (#{user.email}) was created")
- notification_service.new_user(user, @reset_token) if @reset_token
- system_hook_service.execute_hooks_for(user, :create)
- end
+ notify_new_user(user, @reset_token) if user.save
user
end
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 9d7237c2fbb..8e20de8dfa5 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -35,16 +35,18 @@ module Users
Groups::DestroyService.new(group, current_user).execute
end
+ namespace = user.namespace
+ namespace.prepare_for_destroy
+
user.personal_projects.each do |project|
# Skip repository removal because we remove directory with namespace
# that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
+ ::Projects::DestroyService.new(project, current_user, skip_repo: project.legacy_storage?).execute
end
MigrateToGhostUserService.new(user).execute unless options[:hard_delete]
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
- namespace = user.namespace
user_data = user.destroy
namespace.really_destroy!
diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb
index dfbd6016c3f..6188b8a4349 100644
--- a/app/services/users/update_service.rb
+++ b/app/services/users/update_service.rb
@@ -1,5 +1,7 @@
module Users
class UpdateService < BaseService
+ include NewUserNotifier
+
def initialize(user, params = {})
@user = user
@params = params.dup
@@ -10,7 +12,11 @@ module Users
assign_attributes(&block)
+ user_exists = @user.persisted?
+
if @user.save(validate: validate)
+ notify_new_user(@user, nil) unless user_exists
+
success
else
error(@user.errors.full_messages.uniq.join('. '))
@@ -28,6 +34,10 @@ module Users
private
def assign_attributes(&block)
+ if @user.user_synced_attributes_metadata
+ params.except!(*@user.user_synced_attributes_metadata.read_only_attributes)
+ end
+
@user.assign_attributes(params) if params.any?
end
end
diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb
index d232e85cd33..7d1ed768ee8 100644
--- a/app/services/validate_new_branch_service.rb
+++ b/app/services/validate_new_branch_service.rb
@@ -13,7 +13,7 @@ class ValidateNewBranchService < BaseService
end
success
- rescue GitHooksService::PreReceiveError => ex
+ rescue Gitlab::Git::HooksService::PreReceiveError => ex
error(ex.message)
end
end
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 2825478926a..cd99e0b90f9 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -19,7 +19,7 @@ class WebHookService
def initialize(hook, data, hook_name)
@hook = hook
@data = data
- @hook_name = hook_name
+ @hook_name = hook_name.to_s
end
def execute
diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb
index ef70871624b..3298ad104ec 100644
--- a/app/uploaders/personal_file_uploader.rb
+++ b/app/uploaders/personal_file_uploader.rb
@@ -4,7 +4,7 @@ class PersonalFileUploader < FileUploader
end
def self.base_dir
- File.join(root_dir, 'system')
+ File.join(root_dir, '-', 'system')
end
private
diff --git a/app/validators/key_restriction_validator.rb b/app/validators/key_restriction_validator.rb
new file mode 100644
index 00000000000..204be827941
--- /dev/null
+++ b/app/validators/key_restriction_validator.rb
@@ -0,0 +1,29 @@
+class KeyRestrictionValidator < ActiveModel::EachValidator
+ FORBIDDEN = -1
+
+ def self.supported_sizes(type)
+ Gitlab::SSHPublicKey.supported_sizes(type)
+ end
+
+ def self.supported_key_restrictions(type)
+ [0, *supported_sizes(type), FORBIDDEN]
+ end
+
+ def validate_each(record, attribute, value)
+ unless valid_restriction?(value)
+ record.errors.add(attribute, "must be forbidden, allowed, or one of these sizes: #{supported_sizes_message}")
+ end
+ end
+
+ private
+
+ def supported_sizes_message
+ sizes = self.class.supported_sizes(options[:type])
+ sizes.to_sentence(last_word_connector: ', or ', two_words_connector: ' or ')
+ end
+
+ def valid_restriction?(value)
+ choices = self.class.supported_key_restrictions(options[:type])
+ choices.include?(value)
+ end
+end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index a4f49d3f6d7..dbaed1d09fb 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -7,15 +7,15 @@
= f.label :default_branch_protection, class: 'control-label col-sm-2'
.col-sm-10
= f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
- .form-group.project-visibility-level-holder
+ .form-group.visibility-level-setting
= f.label :default_project_visibility, class: 'control-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
- .form-group.project-visibility-level-holder
+ .form-group.visibility-level-setting
= f.label :default_snippet_visibility, class: 'control-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new)
- .form-group.project-visibility-level-holder
+ .form-group.visibility-level-setting
= f.label :default_group_visibility, class: 'control-label col-sm-2'
.col-sm-10
= render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new)
@@ -42,6 +42,14 @@
= link_to "(?)", help_page_path("integration/bitbucket")
and GitLab.com
= link_to "(?)", help_page_path("integration/gitlab")
+
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :project_export_enabled do
+ = f.check_box :project_export_enabled
+ Project export enabled
+
.form-group
%label.control-label.col-sm-2 Enabled Git access protocols
.col-sm-10
@@ -49,6 +57,13 @@
%span.help-block#clone-protocol-help
Allow only the selected protocols to be used for Git access.
+ - ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
+ - field_name = :"#{type}_key_restriction"
+ .form-group
+ = f.label field_name, "#{type.upcase} SSH keys", class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control'
+
%fieldset
%legend Account and Limit Settings
.form-group
@@ -147,7 +162,7 @@
.checkbox
= f.label :password_authentication_enabled do
= f.check_box :password_authentication_enabled
- Password authentication enabled
+ Sign-in enabled
- if omniauth_enabled? && button_based_providers.any?
.form-group
= f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
@@ -211,7 +226,17 @@
.help-block 0 for unlimited
%fieldset
- %legend Continuous Integration
+ %legend Continuous Integration and Deployment
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :auto_devops_enabled do
+ = f.check_box :auto_devops_enabled
+ Enabled Auto DevOps (Beta) for projects by default
+ .help-block
+ It will automatically build, test, and deploy applications based on a predefined CI/CD configuration
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md')
+
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
@@ -356,7 +381,9 @@
%fieldset
%legend Background Jobs
%p
- These settings require a restart to take effect.
+ These settings require a
+ = link_to 'restart', help_page_path('administration/restart_gitlab')
+ to take effect.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
@@ -485,6 +512,16 @@
%fieldset
%legend Repository Storage
.form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :hashed_storage_enabled do
+ = f.check_box :hashed_storage_enabled
+ Create new projects using hashed storage paths
+ .help-block
+ Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
+ repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
+ %em (EXPERIMENTAL)
+ .form-group
= f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
.col-sm-10
= f.select :repository_storages, repository_storages_options_for_select, {include_hidden: false}, multiple: true, class: 'form-control'
@@ -493,6 +530,7 @@
= succeed "." do
= link_to "repository storages documentation", help_page_path("administration/repository_storages")
+
%fieldset
%legend Repository Checks
.form-group
@@ -511,24 +549,27 @@
.help-block
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
- %fieldset
- %legend Koding
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :koding_enabled do
- = f.check_box :koding_enabled
- Enable Koding
- .form-group
- = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
- .help-block
- Koding has integration enabled out of the box for the
- %strong gitlab
- team, and you need to provide that team's URL here. Learn more in the
- = succeed "." do
- = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+ - if koding_enabled?
+ %fieldset
+ %legend Koding
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :koding_enabled do
+ = f.check_box :koding_enabled
+ Enable Koding
+ .help-block
+ Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
+ .form-group
+ = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
+ .help-block
+ Koding has integration enabled out of the box for the
+ %strong gitlab
+ team, and you need to provide that team's URL here. Learn more in the
+ = succeed "." do
+ = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
%fieldset
%legend PlantUML
diff --git a/app/views/admin/applications/edit.html.haml b/app/views/admin/applications/edit.html.haml
index 13b583e6072..13c408914bb 100644
--- a/app/views/admin/applications/edit.html.haml
+++ b/app/views/admin/applications/edit.html.haml
@@ -1,3 +1,5 @@
+- add_to_breadcrumbs "Applications", admin_applications_path
+- breadcrumb_title @application.name
- page_title "Edit", @application.name, "Applications"
%h3.page-title Edit application
diff --git a/app/views/admin/cohorts/_usage_ping.html.haml b/app/views/admin/cohorts/_usage_ping.html.haml
index 73aa95d84f1..3dda386fcf7 100644
--- a/app/views/admin/cohorts/_usage_ping.html.haml
+++ b/app/views/admin/cohorts/_usage_ping.html.haml
@@ -7,4 +7,4 @@
= succeed '.' do
= link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics')
-%pre.usage-data.js-syntax-highlight.code.highlight{ data: { endpoint: usage_data_admin_application_settings_path(format: :html, pretty: true) } }
+%pre.usage-data.js-syntax-highlight.code.highlight{ data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
diff --git a/app/views/admin/cohorts/index.html.haml b/app/views/admin/cohorts/index.html.haml
index be8644c0ca6..bff53da1d9a 100644
--- a/app/views/admin/cohorts/index.html.haml
+++ b/app/views/admin/cohorts/index.html.haml
@@ -1,3 +1,4 @@
+- breadcrumb_title "Cohorts"
- @no_container = true
= render "admin/dashboard/head"
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index dff549f502c..c2151710884 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -31,3 +31,7 @@
= 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 8e94e68bc11..069f8f89e0b 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -1,4 +1,5 @@
- @no_container = true
+- breadcrumb_title "Dashboard"
= render "admin/dashboard/head"
%div{ class: container_class }
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 2aadc071c75..3e02f7b1e16 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -1,3 +1,5 @@
+- add_to_breadcrumbs "Groups", admin_groups_path
+- breadcrumb_title @group.name
- page_title @group.name, "Groups"
%h3.page-title
Group: #{@group.full_name}
diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml
index 665e8c7e74f..efb15ccc8df 100644
--- a/app/views/admin/hooks/edit.html.haml
+++ b/app/views/admin/hooks/edit.html.haml
@@ -1,3 +1,4 @@
+- add_to_breadcrumbs "System Hooks", admin_hooks_path
- page_title 'Edit System Hook'
%h3.page-title
Edit System Hook
diff --git a/app/views/admin/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml
index 09be17f07be..aa6e9db3900 100644
--- a/app/views/admin/jobs/index.html.haml
+++ b/app/views/admin/jobs/index.html.haml
@@ -1,3 +1,4 @@
+- breadcrumb_title "Jobs"
- @no_container = true
= render "admin/dashboard/head"
diff --git a/app/views/admin/labels/edit.html.haml b/app/views/admin/labels/edit.html.haml
index 309aedceded..96f0d404ac4 100644
--- a/app/views/admin/labels/edit.html.haml
+++ b/app/views/admin/labels/edit.html.haml
@@ -1,3 +1,5 @@
+- add_to_breadcrumbs "Labels", admin_labels_path
+- breadcrumb_title "Edit Label"
- page_title "Edit", @label.name, "Labels"
%h3.page-title
Edit Label
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 487f1cf5c4f..ee87f25a225 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -1,25 +1,21 @@
- @no_container = true
- page_title "Logs"
-- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
- Gitlab::EnvironmentLogger, Gitlab::SidekiqLogger,
- Gitlab::RepositoryCheckLogger]
= render 'admin/monitoring/head'
%div{ class: container_class }
%ul.nav-links.log-tabs
- - loggers.each do |klass|
- %li{ class: active_when(klass == Gitlab::GitLogger) }>
- = link_to klass::file_name, "##{klass::file_name_noext}",
- 'data-toggle' => 'tab'
+ - @loggers.each do |klass|
+ %li{ class: active_when(klass == @loggers.first) }>
+ = link_to klass.file_name, "##{klass.file_name_noext}", data: { toggle: 'tab' }
.row-content-block
To prevent performance issues admin logs output the last 2000 lines
.tab-content
- - loggers.each do |klass|
- .tab-pane{ class: active_when(klass == Gitlab::GitLogger), id: klass::file_name_noext }
+ - @loggers.each do |klass|
+ .tab-pane{ class: active_when(klass == @loggers.first), id: klass.file_name_noext }
.file-holder#README
.js-file-title.file-title
%i.fa.fa-file
- = klass::file_name
+ = klass.file_name
.pull-right
= link_to '#', class: 'log-bottom' do
%i.fa.fa-arrow-down
diff --git a/app/views/admin/monitoring/_head.html.haml b/app/views/admin/monitoring/_head.html.haml
index 901e30275fd..b3530915068 100644
--- a/app/views/admin/monitoring/_head.html.haml
+++ b/app/views/admin/monitoring/_head.html.haml
@@ -3,10 +3,6 @@
= render 'shared/nav_scroll'
.nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) }
- = nav_link(controller: :conversational_development_index) do
- = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do
- %span
- ConvDev Index
= nav_link(controller: :system_info) do
= link_to admin_system_info_path, title: 'System Info' do
%span
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 7b1b15cfeb8..ab4165c0bf2 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,3 +1,5 @@
+- add_to_breadcrumbs "Projects", admin_projects_path
+- breadcrumb_title @project.name_with_namespace
- page_title @project.name_with_namespace, "Projects"
%h3.page-title
Project: #{@project.name_with_namespace}
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 126550ee10e..6793ce557c4 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -1,3 +1,4 @@
+- breadcrumb_title "Runners"
- @no_container = true
= render "admin/dashboard/head"
diff --git a/app/views/admin/services/edit.html.haml b/app/views/admin/services/edit.html.haml
index 53d970e33c1..512176649e6 100644
--- a/app/views/admin/services/edit.html.haml
+++ b/app/views/admin/services/edit.html.haml
@@ -1,2 +1,4 @@
+- add_to_breadcrumbs "Service Templates", admin_application_settings_services_path
+- breadcrumb_title @service.title
- page_title @service.title, "Service Templates"
= render 'form'
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 4cf4a57ba18..ca6e43e091c 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -40,9 +40,10 @@
%li
= link_to 'Remove user', admin_user_path(user),
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" },
+ class: 'text-danger',
method: :delete
%li
= link_to 'Remove user and contributions', admin_user_path(user, hard_delete: true),
data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and comments authored by this user, and groups owned solely by them, will also be removed! Are you sure?" },
- class: 'btn btn-remove btn-block',
+ class: 'text-danger',
method: :delete
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index b556ff056c0..98ff592eb64 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -1,3 +1,5 @@
+- add_to_breadcrumbs "Users", admin_users_path
+- breadcrumb_title @user.name
- page_title @user.name, "Users"
= render 'admin/users/head'
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index c91602fcff7..30bf1384b22 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -22,10 +22,10 @@
%b Tag list:
= build[:tag_list].to_a.join(", ")
%br
- %b Refs only:
+ %b Only policy:
= @jobs[build[:name].to_sym][:only].to_a.join(", ")
%br
- %b Refs except:
+ %b Except policy:
= @jobs[build[:name].to_sym][:except].to_a.join(", ")
%br
%b Environment:
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index 8ed23ac4919..dcfb7f0c32d 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -6,14 +6,14 @@
- tooltip = "#{subject.name} - #{status.label}"
- if status.has_details?
- = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip } do
+ = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
%span{ class: klass }= custom_icon(status.icon)
%span.ci-build-text= subject.name
- else
- .mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip } }
+ .menu-item.mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } }
%span{ class: klass }= custom_icon(status.icon)
%span.ci-build-text= subject.name
- if status.has_action?
- = link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do
+ = link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
= custom_icon(status.action_icon)
diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml
index 98f618ca3b8..fbfe3e56588 100644
--- a/app/views/ci/variables/_content.html.haml
+++ b/app/views/ci/variables/_content.html.haml
@@ -1,9 +1,3 @@
-%h4.prepend-top-0
- Secret variables
- = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank'
-%p
- These variables will be set to environment by the runner, and could be protected by exposing only to protected branches or tags.
-%p
- So you can use them for passwords, secret keys or whatever you want.
-%p
- The value of the variable can be visible in job log if explicitly asked to do so.
+%p.append-bottom-default
+ Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags.
+ You can use variables for passwords, secret keys, or whatever you want.
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 007c2344b5a..2bac69bc536 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -1,7 +1,5 @@
.row.prepend-top-default.append-bottom-default
- .col-lg-4
- = render "ci/variables/content"
- .col-lg-8
+ .col-lg-12
%h5.prepend-top-0
Add a variable
= render "ci/variables/form", btn_text: "Add new variable"
diff --git a/app/views/ci/variables/_show.html.haml b/app/views/ci/variables/_show.html.haml
index 2bfb290629d..6d75ae96124 100644
--- a/app/views/ci/variables/_show.html.haml
+++ b/app/views/ci/variables/_show.html.haml
@@ -4,6 +4,6 @@
.col-lg-3
= render "ci/variables/content"
.col-lg-9
- %h5.prepend-top-0
+ %h4.prepend-top-0
Update variable
= render "ci/variables/form", btn_text: "Save variable"
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 5a379eae8f4..7981daa0705 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,7 +1,3 @@
-- if show_new_nav? && current_user.can_create_group?
- - content_for :breadcrumbs_extra do
- = link_to "New group", new_group_path, class: "btn btn-new"
-
.top-area
%ul.nav-links
= nav_link(page: dashboard_groups_path) do
@@ -10,8 +6,8 @@
= nav_link(page: explore_groups_path) do
= link_to explore_groups_path, title: 'Explore public groups' do
Explore public groups
- .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) }
+ .nav-controls
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
- if current_user.can_create_group?
- = link_to "New group", new_group_path, class: "btn btn-new #{("visible-xs" if show_new_nav?)}"
+ = link_to "New group", new_group_path, class: "btn btn-new"
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 1f9a5b401b6..fd2ba9ac1ca 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,10 +1,6 @@
= content_for :flash_message do
= render 'shared/project_limit'
-- if show_new_nav? && current_user.can_create_project?
- - content_for :breadcrumbs_extra do
- = link_to "New project", new_project_path, class: 'btn btn-new'
-
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
@@ -19,8 +15,8 @@
= link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
Explore projects
- .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) }
+ .nav-controls
= render 'shared/projects/search_form'
= render 'shared/projects/dropdown'
- if current_user.can_create_project?
- = link_to "New project", new_project_path, class: "btn btn-new #{("visible-xs" if show_new_nav?)}"
+ = link_to "New project", new_project_path, class: "btn btn-new"
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index fd5389106bb..7330f4cb523 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,7 +1,3 @@
-- if show_new_nav? && current_user
- - content_for :breadcrumbs_extra do
- = link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet"
-
.top-area
%ul.nav-links
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
@@ -12,5 +8,5 @@
Explore Snippets
- if current_user
- .nav-controls.hidden-xs{ class: ("hidden-sm hidden-md hidden-lg" if show_new_nav?) }
+ .nav-controls.hidden-xs
= link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet"
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 52e0012fd7d..42941acc508 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -4,18 +4,12 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{current_user.name} issues")
-- if show_new_nav?
- - content_for :breadcrumbs_extra do
- = link_to params.merge(rss_url_options), class: 'btn has-tooltip append-right-10', title: 'Subscribe' do
- = icon('rss')
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues'
-
.top-area
= render 'shared/issuable/nav', type: :issues
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls
= link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do
= icon('rss')
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues'
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", with_feature_enabled: 'issues', type: :issues
= render 'shared/issuable/filter', type: :issues
= render 'shared/issues'
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index c3fe14da2b2..53cd1130299 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -2,14 +2,10 @@
- page_title "Merge Requests"
- header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id)
-- if show_new_nav?
- - content_for :breadcrumbs_extra do
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests'
-
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests'
+ .nav-controls
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", with_feature_enabled: 'merge_requests', type: :merge_requests
= render 'shared/issuable/filter', type: :merge_requests
= render 'shared/merge_requests'
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index 37dbcaf5cb8..f66e2b40d76 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -2,15 +2,11 @@
- page_title 'Milestones'
- header_title 'Milestones', dashboard_milestones_path
-- if show_new_nav?
- - content_for :breadcrumbs_extra do
- = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true
-
.top-area
= render 'shared/milestones_filter', counts: @milestone_states
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
- = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true
+ .nav-controls
+ = render 'shared/new_project_item_select', path: 'milestones/new', label: 'New milestone', include_groups: true, type: :milestones
.milestones
%ul.content-list
diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
index 209afd4aab4..57544559824 100644
--- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
@@ -28,6 +28,6 @@
%h3.blank-state-title
Create a group
%p.blank-state-text
- Groups are a great way to organise projects and people.
+ Groups are a great way to organize projects and people.
= link_to new_group_path, class: "btn btn-new" do
New group
diff --git a/app/views/discussions/_headline.html.haml b/app/views/discussions/_headline.html.haml
index c1dabeed387..b865eb815f0 100644
--- a/app/views/discussions/_headline.html.haml
+++ b/app/views/discussions/_headline.html.haml
@@ -1,11 +1,13 @@
- if discussion.resolved?
.discussion-headline-light.js-discussion-headline
- Resolved
+ = discussion_resolved_intro(discussion)
- if discussion.resolved_by
by
= link_to_member(@project, discussion.resolved_by, avatar: false)
+ - if discussion.resolved_by_push?
+ with a push
= time_ago_with_tooltip(discussion.resolved_at, placement: "bottom")
-- elsif discussion.last_updated_at != discussion.created_at
+- elsif discussion.updated?
.discussion-headline-light.js-discussion-headline
Last updated
- if discussion.last_updated_by
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index 82aa51f9778..8ba88906714 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -1,39 +1,43 @@
-%h3.page-title Authorization required
%main{ :role => "main" }
- %p.h4
- Authorize
- %strong.text-info= @pre_auth.client.name
- to use your account?
+ .modal-no-backdrop
+ .modal-content
+ .modal-header
+ %h3.page-title
+ Authorize
+ = link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
+ to use your account?
- - if current_user.admin?
- .text-warning.prepend-top-20
- %p
- = icon("exclamation-triangle fw")
- You are an admin, which means granting access to
- %strong= @pre_auth.client.name
- will allow them to interact with GitLab as an admin as well. Proceed with caution.
-
- - if @pre_auth.scopes
- #oauth-permissions
- %p This application will be able to:
- %ul.text-info
- - @pre_auth.scopes.each do |scope|
- %li= t scope, scope: [:doorkeeper, :scopes]
- %hr/
- .actions
- = form_tag oauth_authorization_path, method: :post do
- = hidden_field_tag :client_id, @pre_auth.client.uid
- = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
- = hidden_field_tag :state, @pre_auth.state
- = hidden_field_tag :response_type, @pre_auth.response_type
- = hidden_field_tag :scope, @pre_auth.scope
- = hidden_field_tag :nonce, @pre_auth.nonce
- = submit_tag "Authorize", class: "btn btn-success wide pull-left"
- = form_tag oauth_authorization_path, method: :delete do
- = hidden_field_tag :client_id, @pre_auth.client.uid
- = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
- = hidden_field_tag :state, @pre_auth.state
- = hidden_field_tag :response_type, @pre_auth.response_type
- = hidden_field_tag :scope, @pre_auth.scope
- = hidden_field_tag :nonce, @pre_auth.nonce
- = submit_tag "Deny", class: "btn btn-danger prepend-left-10"
+ .modal-body
+ - if current_user.admin?
+ .text-warning
+ %p
+ = icon("exclamation-triangle fw")
+ You are an admin, which means granting access to
+ %strong= @pre_auth.client.name
+ will allow them to interact with GitLab as an admin as well. Proceed with caution.
+ %p
+ You are about to authorize
+ = link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
+ to use your account.
+ - if @pre_auth.scopes
+ This application will be able to:
+ %ul
+ - @pre_auth.scopes.each do |scope|
+ %li= t scope, scope: [:doorkeeper, :scopes]
+ .form-actions.text-right
+ = form_tag oauth_authorization_path, method: :delete, class: 'inline' do
+ = hidden_field_tag :client_id, @pre_auth.client.uid
+ = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
+ = hidden_field_tag :state, @pre_auth.state
+ = hidden_field_tag :response_type, @pre_auth.response_type
+ = hidden_field_tag :scope, @pre_auth.scope
+ = hidden_field_tag :nonce, @pre_auth.nonce
+ = submit_tag "Deny", class: "btn btn-danger"
+ = form_tag oauth_authorization_path, method: :post, class: 'inline' do
+ = hidden_field_tag :client_id, @pre_auth.client.uid
+ = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
+ = hidden_field_tag :state, @pre_auth.state
+ = hidden_field_tag :response_type, @pre_auth.response_type
+ = hidden_field_tag :scope, @pre_auth.scope
+ = hidden_field_tag :nonce, @pre_auth.nonce
+ = submit_tag "Authorize", class: "btn btn-success prepend-left-10"
diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml
index ad434a64556..98cdcca3ecc 100644
--- a/app/views/events/_commit.html.haml
+++ b/app/views/events/_commit.html.haml
@@ -1,5 +1,5 @@
%li.commit
.commit-row-title
- = link_to truncate_sha(commit[:id]), project_commit_path(project, commit[:id]), class: "commit-sha", alt: '', title: truncate_sha(commit[:id])
+ = link_to truncate_sha(event.commit_id), project_commit_path(project, event.commit_id), class: "commit-sha", alt: '', title: truncate_sha(event.commit_id)
&middot;
- = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line, author: event.author
+ = markdown event_commit_title(event.commit_title), project: project, pipeline: :single_line, author: event.author
diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml
index 9fcacfbbf36..e3c5fd55f08 100644
--- a/app/views/events/_event_push.atom.haml
+++ b/app/views/events/_event_push.atom.haml
@@ -1,14 +1,14 @@
%div{ xmlns: "http://www.w3.org/1999/xhtml" }
- - event.commits.first(15).each do |commit|
- %p
- %strong= commit[:author][:name]
- = link_to "(##{truncate_sha(commit[:id])})", project_commit_path(event.project, id: commit[:id])
- %i
- at
- = commit[:timestamp].to_time.to_s(:short)
- %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project, author: event.author)
- - if event.commits_count > 15
- %p
- %i
- \... and
- = pluralize(event.commits_count - 15, "more commit")
+ %p
+ %strong= event.author_name
+ = link_to "(#{truncate_sha(event.commit_id)})", project_commit_path(event.project, event.commit_id)
+ %i
+ at
+ = event.created_at.to_s(:short)
+ - unless event.rm_ref?
+ %blockquote= markdown(escape_once(event.commit_title), pipeline: :atom, project: event.project, author: event.author)
+ - if event.commits_count > 1
+ %p
+ %i
+ \... and
+ = pluralize(event.commits_count - 1, "more commit")
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 54b414cc62a..53ebdd6d2ff 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -14,9 +14,7 @@
- if event.push_with_commits?
.event-body
%ul.well-list.event_commits
- - few_commits = event.commits[0...2]
- - few_commits.each do |commit|
- = render "events/commit", commit: commit, project: project, event: event
+ = render "events/commit", project: project, event: event
- 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
@@ -43,10 +41,3 @@
%li.commits-stat
= link_to create_mr_path(project.default_branch, event.ref_name, project) do
Create Merge Request
-- elsif event.rm_ref?
- - repository = project.repository
- - last_commit = repository.commit(event.commit_from)
- - if last_commit
- .event-body
- %ul.well-list.event_commits
- = render "events/commit", commit: last_commit, project: project, event: event
diff --git a/app/views/feature_highlight/_issue_boards.svg b/app/views/feature_highlight/_issue_boards.svg
new file mode 100644
index 00000000000..1522c9d51c9
--- /dev/null
+++ b/app/views/feature_highlight/_issue_boards.svg
@@ -0,0 +1,98 @@
+<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/edit.html.haml b/app/views/groups/edit.html.haml
index 9ebb3894c55..0d3308833b7 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,3 +1,4 @@
+- breadcrumb_title "General Settings"
= render "groups/settings_head"
.panel.panel-default.prepend-top-default
.panel-heading
@@ -27,17 +28,20 @@
.col-sm-offset-2.col-sm-10
= render 'shared/allow_request_access', form: f
- = render 'group_admin_settings', f: f
-
.form-group
- %hr
- = f.label :share_with_group_lock, class: 'control-label' do
- Share with group lock
+ %label.control-label
+ = s_("GroupSettings|Share with group lock")
.col-sm-10
.checkbox
- = f.check_box :share_with_group_lock
- %span.descr Prevent sharing a project with another group within this group
+ = f.label :share_with_group_lock do
+ = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group)
+ %strong
+ - group_link = link_to @group.name, group_path(@group)
+ = s_("GroupSettings|Prevent sharing a project within %{group} with other groups").html_safe % { group: group_link }
+ %br
+ %span.descr= share_with_group_lock_help_text(@group)
+ = render 'group_admin_settings', f: f
.form-actions
= f.submit 'Save group', class: "btn btn-save"
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index f83ebbf09ef..7f411927429 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -8,21 +8,15 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
-- if show_new_nav? && group_issues_exists
- - content_for :breadcrumbs_extra do
- = link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10' do
- = icon('rss')
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue"
-
- if group_issues_exists
.top-area
= render 'shared/issuable/nav', type: :issues
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls
= link_to params.merge(rss_url_options), class: 'btn' do
= icon('rss')
%span.icon-label
Subscribe
- = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue"
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues
= render 'shared/issuable/search_bar', type: :issues
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 50179a47797..89165096fe2 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -1,7 +1,4 @@
- page_title 'Labels'
-- if show_new_nav? && can?(current_user, :admin_label, @group)
- - content_for :breadcrumbs_extra do
- = link_to "New label", new_group_label_path(@group), class: "btn btn-new"
= render "groups/head_issues"
@@ -10,7 +7,7 @@
.nav-text
Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls
- if can?(current_user, :admin_label, @group)
= link_to "New label", new_group_label_path(@group), class: "btn btn-new"
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 997c82c77d9..e56dc1fb9c2 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,8 +1,8 @@
- page_title "Merge Requests"
-- if show_new_nav? && current_user
- - content_for :breadcrumbs_extra do
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request"
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'filtered_search'
- if @group_merge_requests.empty?
= render 'shared/empty_states/merge_requests', project_select_button: true
@@ -10,10 +10,10 @@
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- if current_user
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request"
+ .nav-controls
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests
- = render 'shared/issuable/filter', type: :merge_requests
+ = render 'shared/issuable/search_bar', type: :merge_requests
.row-content-block.second-block
Only merge requests from
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index 66c6cc9e279..ed582e521c4 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -1,14 +1,11 @@
- page_title "Milestones"
-- if show_new_nav? && can?(current_user, :admin_milestones, @group)
- - content_for :breadcrumbs_extra do
- = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new"
= render "groups/head_issues"
.top-area
= render 'shared/milestones_filter', counts: @milestone_states
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls
- if can?(current_user, :admin_milestones, @group)
= link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new"
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index 54b1b7a734a..23b1a22240f 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,4 +1,4 @@
= render "header_title"
= render 'shared/milestones/top', milestone: @milestone, group: @group
-= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true if @milestone.is_legacy_group_milestone?
+= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true if @milestone.legacy_group_milestone?
= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 7a2e688a114..7f3f2f707f7 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,3 +1,4 @@
+- breadcrumb_title "Projects"
= render "groups/settings_head"
.panel.panel-default.prepend-top-default
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index bf36baf48ab..9f9ae01e7c5 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -1,4 +1,5 @@
-- page_title "Pipelines"
+- 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 e07f61c94e4..f4f76887422 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- breadcrumb_title "Group"
+- breadcrumb_title "Details"
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml
index 8f0724c0677..7abc84412c6 100644
--- a/app/views/groups/subgroups.html.haml
+++ b/app/views/groups/subgroups.html.haml
@@ -1,3 +1,4 @@
+- breadcrumb_title "Details"
- @no_container = true
= render 'head'
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index f18c3a74120..445f0dffbcc 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -189,7 +189,7 @@
= icon('chevron-down')
%ul.dropdown-menu
%li
- %a Sort by date
+ = link_to 'Sort by date', '#'
= link_to 'New issue', '#', class: 'btn btn-new btn-inverted'
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index c52a515226e..84e0009487f 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -10,7 +10,7 @@
Customize how FogBugz email addresses and usernames are imported into GitLab.
In the next step, you'll be able to select the projects you want to import.
%p
- The user map is a mapping of the FogBugz users that participated on your projects to the way their email address and usernames wil be imported into GitLab. You can change this by populating the table below.
+ The user map is a mapping of the FogBugz users that participated on your projects to the way their email address and usernames will be imported into GitLab. You can change this by populating the table below.
%ul
%li
%strong Default: Map a FogBugz account ID to a full name
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 008e8287aa3..5d68e1e2156 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -25,7 +25,7 @@
= hidden_field_tag :namespace_id, value: current_user.namespace_id
.form-group.col-xs-12.col-sm-6.project-path
= label_tag :path, 'Project name', class: 'label-light'
- = text_field_tag :path, nil, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, autofocus: true, required: true
+ = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, autofocus: true, required: true
.row
.form-group.col-md-12
@@ -33,7 +33,6 @@
.row
.form-group.col-sm-12
= hidden_field_tag :namespace_id, @namespace.id
- = hidden_field_tag :path, @path
= label_tag :file, 'GitLab project export', class: 'label-light'
.form-group
= file_field_tag :file, class: ''
diff --git a/app/views/kaminari/gitlab/_without_count.html.haml b/app/views/kaminari/gitlab/_without_count.html.haml
new file mode 100644
index 00000000000..250029c4475
--- /dev/null
+++ b/app/views/kaminari/gitlab/_without_count.html.haml
@@ -0,0 +1,8 @@
+.gl-pagination
+ %ul.pagination.clearfix
+ - if previous_path
+ %li.prev
+ = link_to(t('views.pagination.previous'), previous_path, rel: 'prev')
+ - if next_path
+ %li.next
+ = link_to(t('views.pagination.next'), next_path, rel: 'next')
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 3babdae3968..34e85fef6d9 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -32,9 +32,9 @@
= stylesheet_link_tag "test", media: "all" if Rails.env.test?
= stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
- - if show_new_nav?
- = stylesheet_link_tag "new_nav", media: "all"
- = stylesheet_link_tag "new_sidebar", media: "all"
+ // 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
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index c4f8cd71395..1fd301d6850 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,26 +1,14 @@
.page-with-sidebar{ class: page_with_sidebar_class }
- - if show_new_nav?
- - if defined?(nav) && nav
- = render "layouts/nav/#{nav}"
- - else
- - if defined?(nav) && nav
- .layout-nav
- .container-fluid
- = render "layouts/nav/#{nav}"
- - if content_for?(:sub_nav)
- = yield :sub_nav
- .content-wrapper{ class: layout_nav_class }
- - if show_new_nav?
- .mobile-overlay
+ - if defined?(nav) && nav
+ = render "layouts/nav/sidebar/#{nav}"
+ .content-wrapper.page-with-new-nav
+ .mobile-overlay
.alert-wrapper
= render "layouts/broadcast"
- - if show_new_nav?
- - if content_for?(:new_global_flash)
- = yield :new_global_flash
- - unless @hide_breadcrumbs
- = render "layouts/nav/breadcrumbs"
- = render "layouts/flash"
= yield :flash_message
+ - unless @hide_breadcrumbs
+ = render "layouts/nav/breadcrumbs"
+ = render "layouts/flash"
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content{ id: "content-body" }
= yield
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 59f16b47bf7..cd7a47da4a1 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -17,8 +17,8 @@
.dropdown-menu.dropdown-select
= dropdown_content do
%ul
- %li
- %a.is-focused.dropdown-menu-empty-link
+ %li.dropdown-menu-empty-item
+ %a
Loading...
= dropdown_loading
%i.search-icon
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index ae9eee215e0..8595157a997 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -1,9 +1,6 @@
- page_title "Admin Area"
- header_title "Admin Area", admin_root_path
-- if show_new_nav?
- - nav "new_admin_sidebar"
- - @new_sidebar = true
-- else
- - nav "admin"
+- nav "admin"
+- @left_sidebar = true
= render template: "layouts/application"
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index b53f382fa3d..0ca34b276a7 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,13 +1,10 @@
!!! 5
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
- %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
+ %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
= render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar'
- - if show_new_nav?
- = render "layouts/header/new"
- - else
- = render "layouts/header/default", title: header_title
+ = render "layouts/header/default"
= render 'layouts/page', sidebar: sidebar, nav: nav
= yield :scripts_body
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index 6d9ec043590..9382ee8715e 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -15,7 +15,7 @@
h1 {
font-size: 56px;
line-height: 100px;
- font-weight: normal;
+ font-weight: 400;
color: #456;
}
@@ -28,7 +28,7 @@
h3 {
color: #456;
font-size: 20px;
- font-weight: normal;
+ font-weight: 400;
line-height: 28px;
}
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index 35abfa0e80c..08bd6fc311e 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -1,10 +1,7 @@
- page_title @group.name
- page_description @group.description unless page_description
- header_title group_title(@group) unless header_title
-- if show_new_nav?
- - nav "new_group_sidebar"
- - @new_sidebar = true
-- else
- - nav "group"
+- nav "group"
+- @left_sidebar = true
= render template: "layouts/application"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index df1dc736571..d8fc371497d 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,68 +1,50 @@
-%header.navbar.navbar-gitlab{ class: nav_header_class }
- .navbar-border
+%header.navbar.navbar-gitlab.navbar-gitlab-new
%a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
.container-fluid
.header-content
- .dropdown.global-dropdown
- %button.global-dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span.sr-only Toggle navigation
- = icon('bars')
- .dropdown-menu-nav.global-dropdown-menu
- - if current_user
- = render 'layouts/nav/dashboard'
- - else
- = render 'layouts/nav/explore'
+ .title-container
+ %h1.title
+ = link_to root_path, title: 'Dashboard', id: 'logo' do
+ = brand_header_logo
+ %span.logo-text.hidden-xs
+ = render 'shared/logo_type.svg'
- .header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
- = brand_header_logo
-
- .title-container.js-title-container
- %h1.title{ class: ('initializing' if @has_group_title) }= title
+ - if current_user
+ = render "layouts/nav/dashboard"
+ - else
+ = render "layouts/nav/explore"
.navbar-collapse.collapse
%ul.nav.navbar-nav
+ - if current_user
+ = render 'layouts/header/new_dropdown'
%li.hidden-sm.hidden-xs
= 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')
- if current_user
- - if session[:impersonator_id]
- %li.impersonation
- = link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
- = icon('user-secret fw')
- - if current_user.admin?
- %li
- = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('wrench fw')
- = render 'layouts/header/new_dropdown'
- - if Gitlab::Sherlock.enabled?
- %li
- = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
- data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('tachometer fw')
- %li
- = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ %li.user-counter
+ = 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')
- issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
- %li
- = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ %li.user-counter
+ = 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')
- 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
+ %li.user-counter
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('check-circle fw')
+ = custom_icon('todo_done')
%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: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
- = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
- = icon('caret-down')
+ = 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')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
%li.current-user
@@ -74,19 +56,24 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li
= link_to "Settings", profile_path
- = render 'shared/user_dropdown_experimental_features'
+ - if current_user
+ %li
+ = link_to "Help", help_path
%li.divider
%li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
+ - if session[:impersonator_id]
+ %li.impersonation
+ = link_to admin_impersonation_path, class: 'impersonation-btn', method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = icon('user-secret')
- else
%li
%div
- = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
+ = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
- %button.navbar-toggle{ type: 'button' }
+ %button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation
- = icon('ellipsis-v')
-
- = yield :header_content
+ = icon('ellipsis-v', class: 'js-navbar-toggle-right')
+ = icon('times', class: 'js-navbar-toggle-left')
= render 'shared/outdated_browser'
diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml
deleted file mode 100644
index fa94925d537..00000000000
--- a/app/views/layouts/header/_new.html.haml
+++ /dev/null
@@ -1,85 +0,0 @@
-%header.navbar.navbar-gitlab.navbar-gitlab-new{ class: nav_header_class }
- %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content
- .container-fluid
- .header-content
- .title-container
- %h1.title
- = link_to root_path, title: 'Dashboard' do
- = brand_header_logo
- %span.logo-text.hidden-xs
- = render 'shared/logo_type.svg'
-
- - if current_user
- = render "layouts/nav/new_dashboard"
- - else
- = render "layouts/nav/new_explore"
-
- .navbar-collapse.collapse
- %ul.nav.navbar-nav
- %li.hidden-sm.hidden-xs
- = 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')
- - if current_user
- - if session[:impersonator_id]
- %li.impersonation
- = link_to admin_impersonation_path, method: :delete, title: "Stop impersonation", aria: { label: 'Stop impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
- = icon('user-secret fw')
- - if current_user.admin?
- %li
- = link_to admin_root_path, title: 'Admin area', aria: { label: "Admin area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('wrench fw')
- = render 'layouts/header/new_dropdown'
- - if Gitlab::Sherlock.enabled?
- %li
- = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
- data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('tachometer fw')
- %li
- = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = custom_icon('issues')
- - issues_count = assigned_issuables_count(:issues)
- %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
- = number_with_delimiter(issues_count)
- %li
- = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = custom_icon('mr_bold')
- - merge_requests_count = assigned_issuables_count(:merge_requests)
- %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
- = number_with_delimiter(merge_requests_count)
- %li
- = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('check-circle fw')
- %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: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
- = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
- = icon('chevron-down')
- .dropdown-menu-nav.dropdown-menu-align-right
- %ul
- %li.current-user
- .user-name.bold
- = current_user.name
- @#{current_user.username}
- %li.divider
- %li
- = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
- %li
- = link_to "Settings", profile_path
- = render 'shared/user_dropdown_experimental_features'
- %li.divider
- %li
- = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
- - else
- %li
- %div
- = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
-
- %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')
-
-= render 'shared/outdated_browser'
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index 9da739b0974..63d1c077ecd 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -1,11 +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
- - if show_new_nav?
- = icon('plus')
- = icon('chevron-down')
- - else
- = icon('plus fw')
- = icon('caret-down')
+ = custom_icon('plus_square')
+ = custom_icon('caret_down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
- if @group&.persisted?
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
deleted file mode 100644
index 6df0adfd742..00000000000
--- a/app/views/layouts/nav/_admin.html.haml
+++ /dev/null
@@ -1,40 +0,0 @@
-= render 'layouts/nav/admin_settings'
-.scrolling-tabs-container{ class: nav_control_class }
- .fade-left
- = icon('angle-left')
- .fade-right
- = icon('angle-right')
- %ul.nav-links.scrolling-tabs
- = nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
- %span
- Overview
- = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do
- = link_to admin_conversational_development_index_path, title: 'Monitoring' do
- %span
- Monitoring
- = nav_link(controller: :broadcast_messages) do
- = link_to admin_broadcast_messages_path, title: 'Messages' do
- %span
- Messages
- = nav_link(controller: [:hooks, :hook_logs]) do
- = link_to admin_hooks_path, title: 'Hooks' do
- %span
- System Hooks
-
- = nav_link(controller: :applications) do
- = link_to admin_applications_path, title: 'Applications' do
- %span
- Applications
-
- = nav_link(controller: :abuse_reports) do
- = link_to admin_abuse_reports_path, title: "Abuse Reports" do
- %span
- Abuse Reports
- %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
-
- - if akismet_enabled?
- = nav_link(controller: :spam_logs) do
- = link_to admin_spam_logs_path, title: "Spam Logs" do
- %span
- Spam Logs
diff --git a/app/views/layouts/nav/_admin_settings.html.haml b/app/views/layouts/nav/_admin_settings.html.haml
deleted file mode 100644
index 9de0e12a826..00000000000
--- a/app/views/layouts/nav/_admin_settings.html.haml
+++ /dev/null
@@ -1,31 +0,0 @@
-.controls
- .dropdown.admin-settings-dropdown
- %a.dropdown-new.btn.btn-default{ href: '#', 'data-toggle' => 'dropdown' }
- = icon('cog')
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- = nav_link(controller: :deploy_keys) do
- = link_to admin_deploy_keys_path, title: 'Deploy Keys' do
- %span
- Deploy Keys
-
- = nav_link(controller: :services) do
- = link_to admin_application_settings_services_path, title: 'Service Templates' do
- %span
- Service Templates
-
- = nav_link(controller: :labels) do
- = link_to admin_labels_path, title: 'Labels' do
- %span
- Labels
-
- = nav_link(controller: :appearances) do
- = link_to admin_appearances_path, title: 'Appearances' do
- %span
- Appearance
-
- %li.divider
- = nav_link(controller: :application_settings) do
- = link_to admin_application_settings_path, title: 'Settings' do
- %span
- Settings
diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml
index 4db84771f4e..7bd3f5306a2 100644
--- a/app/views/layouts/nav/_breadcrumbs.html.haml
+++ b/app/views/layouts/nav/_breadcrumbs.html.haml
@@ -1,27 +1,20 @@
-- breadcrumb_link = breadcrumb_title_link
+- container = @no_breadcrumb_container ? 'container-fluid' : container_class
- hide_top_links = @hide_top_links || false
-%nav.breadcrumbs{ role: "navigation" }
- .breadcrumbs-container{ class: [container_class, @content_class] }
- - if defined?(@new_sidebar)
+%nav.breadcrumbs{ role: "navigation", class: [container, @content_class] }
+ .breadcrumbs-container
+ - if defined?(@left_sidebar)
= button_tag class: 'toggle-mobile-nav', type: 'button' do
%span.sr-only Open sidebar
= icon ('bars')
.breadcrumbs-links.js-title-container
- - unless hide_top_links
- .title
- = link_to "GitLab", root_path
- \/
- - if content_for?(:header_title_before)
- = yield :header_title_before
- \/
+ %ul.list-unstyled.breadcrumbs-list.js-breadcrumbs-list
+ - unless hide_top_links
= header_title
- %h2.breadcrumbs-sub-title
- %ul.list-unstyled
- - if @breadcrumbs_extra_links
- - @breadcrumbs_extra_links.each do |extra|
- %li= link_to extra[:text], extra[:link]
- %li= link_to @breadcrumb_title, breadcrumb_link
- - if content_for?(:breadcrumbs_extra)
- .breadcrumbs-extra.hidden-xs= yield :breadcrumbs_extra
+ - if @breadcrumbs_extra_links
+ - @breadcrumbs_extra_links.each do |extra|
+ = breadcrumb_list_item link_to(extra[:text], extra[:link])
+ = render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after
+ %li
+ %h2.breadcrumbs-sub-title= @breadcrumb_title
= yield :header_content
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index be7d27df2a0..c254ee02dd8 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,67 +1,62 @@
-%ul
- = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
- = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- P
- %span
- Projects
- = nav_link(path: 'dashboard#activity') do
- = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- A
- %span
- Activity
- - if koding_enabled?
- = nav_link(controller: :koding) do
- = link_to koding_path, title: 'Koding' do
- %span
- Koding
- = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
+%ul.list-unstyled.navbar-sub-nav
+ = 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')
+ .dropdown-menu.projects-dropdown-menu
+ = render "layouts/nav/projects_dropdown/show"
+
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "hidden-xs" }) do
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- G
- %span
- Groups
- = nav_link(controller: 'dashboard/milestones') do
+ Groups
+
+ = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do
+ = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
+ Activity
+
+ = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do
= link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- L
- %span
- Milestones
- = nav_link(path: 'dashboard#issues') do
- = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- I
- %span.badge.pull-right= number_with_delimiter(assigned_issuables_count(:issues))
- %span
- Issues
- = nav_link(path: 'dashboard#merge_requests') do
- = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- M
- %span.badge.pull-right= number_with_delimiter(assigned_issuables_count(:merge_requests))
- %span
- Merge Requests
- = nav_link(controller: 'dashboard/snippets') do
+ Milestones
+
+ = nav_link(controller: 'dashboard/snippets', html_options: { class: "visible-lg" }) do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- S
- %span
- Snippets
- %li.divider
- %li
- = link_to "Help", help_path, title: 'About GitLab CE', class: 'about-gitlab'
+ Snippets
+
+ %li.header-more.dropdown.hidden-lg
+ %a{ href: "#", data: { toggle: "dropdown" } }
+ More
+ = custom_icon('caret_down')
+ .dropdown-menu
+ %ul
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "visible-xs" }) do
+ = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
+ Groups
+
+ = nav_link(path: 'dashboard#activity') do
+ = link_to activity_dashboard_path, title: 'Activity' do
+ Activity
+
+ = nav_link(controller: 'dashboard/milestones') do
+ = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
+ Milestones
+
+ = nav_link(controller: 'dashboard/snippets') do
+ = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
+ Snippets
+
+ -# Shortcut to Dashboard > Projects
+ %li.hidden
+ = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
+ Projects
+
+ - if current_user.admin? || Gitlab::Sherlock.enabled?
+ %li.line-separator.hidden-xs
+ - 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')
+ - if Gitlab::Sherlock.enabled?
+ %li
+ = link_to sherlock_transactions_path, class: 'admin-icon', title: 'Sherlock Transactions',
+ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('tachometer fw')
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index 0cb367452f7..cd1c39f3226 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -1,30 +1,12 @@
-%ul
+%ul.list-unstyled.navbar-sub-nav
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
= link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- P
- %span
- Projects
+ Projects
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- G
- %span
- Groups
+ Groups
= nav_link(controller: :snippets) do
= link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
- .shortcut-mappings
- .key
- = icon('arrow-up', 'aria-label' => 'hidden')
- S
- %span
- Snippets
- %li.divider
- = nav_link(controller: :help) do
- = link_to help_path, title: 'Help' do
- %span
- Help
+ Snippets
+ %li
+ = link_to "Help", help_path, title: 'About GitLab CE'
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
deleted file mode 100644
index 261445ecd2b..00000000000
--- a/app/views/layouts/nav/_group.html.haml
+++ /dev/null
@@ -1,31 +0,0 @@
-.scrolling-tabs-container{ class: nav_control_class }
- .fade-left
- = icon('angle-left')
- .fade-right
- = icon('angle-right')
- %ul.nav-links.scrolling-tabs
- = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
- = link_to group_path(@group), title: 'Home' do
- %span
- Group
- = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
- = link_to issues_group_path(@group), title: 'Issues' do
- %span
- Issues
- - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- %span.badge.count= number_with_delimiter(issues.count)
- = nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
- %span
- Merge Requests
- - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
- %span.badge.count= number_with_delimiter(merge_requests.count)
- = nav_link(path: 'group_members#index') do
- = link_to group_group_members_path(@group), title: 'Members' do
- %span
- Members
- - if current_user && can?(current_user, :admin_group, @group)
- = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
- = link_to edit_group_path(@group), title: 'Settings' do
- %span
- Settings
diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/_new_admin_sidebar.html.haml
deleted file mode 100644
index 0b4a9d92bea..00000000000
--- a/app/views/layouts/nav/_new_admin_sidebar.html.haml
+++ /dev/null
@@ -1,150 +0,0 @@
-.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
- .context-header
- = link_to admin_root_path, title: 'Admin Overview' do
- .avatar-container.s40.settings-avatar
- = icon('wrench')
- .project-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
- = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
- .nav-icon-container
- = custom_icon('overview')
- %span.nav-item-name
- Overview
-
- %ul.sidebar-sub-level-items
- = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview' do
- %span
- Dashboard
- = 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: 'jobs#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: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do
- = link_to admin_conversational_development_index_path, title: 'Monitoring' do
- .nav-icon-container
- = custom_icon('monitoring')
- %span.nav-item-name
- Monitoring
-
- %ul.sidebar-sub-level-items
- = nav_link(controller: :conversational_development_index) do
- = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do
- %span
- ConvDev Index
- = 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
-
- = nav_link(controller: :broadcast_messages) do
- = link_to admin_broadcast_messages_path, title: 'Messages' do
- .nav-icon-container
- = custom_icon('messages')
- %span.nav-item-name
- Messages
- = nav_link(controller: [:hooks, :hook_logs]) do
- = link_to admin_hooks_path, title: 'Hooks' do
- .nav-icon-container
- = custom_icon('system_hooks')
- %span.nav-item-name
- System Hooks
-
- = nav_link(controller: :applications) do
- = link_to admin_applications_path, title: 'Applications' do
- .nav-icon-container
- = custom_icon('applications')
- %span.nav-item-name
- Applications
-
- = nav_link(controller: :abuse_reports) do
- = link_to admin_abuse_reports_path, title: "Abuse Reports" do
- .nav-icon-container
- = custom_icon('abuse_reports')
- %span.nav-item-name
- Abuse Reports
- %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
-
- - if akismet_enabled?
- = nav_link(controller: :spam_logs) do
- = link_to admin_spam_logs_path, title: "Spam Logs" do
- .nav-icon-container
- = custom_icon('spam_logs')
- %span.nav-item-name
- Spam Logs
-
- = nav_link(controller: :deploy_keys) do
- = link_to admin_deploy_keys_path, title: 'Deploy Keys' do
- .nav-icon-container
- = custom_icon('key')
- %span.nav-item-name
- Deploy Keys
-
- = nav_link(controller: :services) do
- = link_to admin_application_settings_services_path, title: 'Service Templates' do
- .nav-icon-container
- = custom_icon('service_templates')
- %span.nav-item-name
- Service Templates
-
- = nav_link(controller: :labels) do
- = link_to admin_labels_path, title: 'Labels' do
- .nav-icon-container
- = custom_icon('labels')
- %span.nav-item-name
- Labels
-
- = nav_link(controller: :appearances) do
- = link_to admin_appearances_path, title: 'Appearances' do
- .nav-icon-container
- = custom_icon('appearance')
- %span.nav-item-name
- Appearance
-
- %li.divider
- = nav_link(controller: :application_settings) do
- = link_to admin_application_settings_path, title: 'Settings' do
- .nav-icon-container
- = custom_icon('settings')
- %span.nav-item-name
- Settings
-
- = render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/_new_dashboard.html.haml b/app/views/layouts/nav/_new_dashboard.html.haml
deleted file mode 100644
index cfdfcbebc9f..00000000000
--- a/app/views/layouts/nav/_new_dashboard.html.haml
+++ /dev/null
@@ -1,33 +0,0 @@
-%ul.list-unstyled.navbar-sub-nav
- = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "home"}) do
- = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- Projects
-
- = nav_link(controller: ['dashboard/groups', 'explore/groups']) do
- = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
- Groups
-
- = nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm" }) do
- = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
- Activity
-
- %li.dropdown
- %a{ href: "#", data: { toggle: "dropdown" } }
- More
- = icon("chevron-down", class: "dropdown-chevron")
- .dropdown-menu
- %ul
- = nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm" }) do
- = link_to activity_dashboard_path, title: 'Activity' do
- Activity
-
- = nav_link(controller: 'dashboard/milestones') do
- = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' do
- Milestones
-
- = nav_link(controller: 'dashboard/snippets') do
- = link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
- Snippets
- %li.divider
- %li
- = link_to "Help", help_path, title: 'About GitLab CE'
diff --git a/app/views/layouts/nav/_new_explore.html.haml b/app/views/layouts/nav/_new_explore.html.haml
deleted file mode 100644
index 40385f251e3..00000000000
--- a/app/views/layouts/nav/_new_explore.html.haml
+++ /dev/null
@@ -1,19 +0,0 @@
-%ul.list-unstyled.navbar-sub-nav
- = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
- = link_to explore_root_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- Projects
- = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
- = link_to explore_groups_path, title: 'Groups', class: 'dashboard-shortcuts-groups' do
- Groups
- %li.dropdown
- %a{ href: "#", data: { toggle: "dropdown" } }
- More
- = icon("chevron-down", class: "dropdown-chevron")
- .dropdown-menu
- %ul
- = nav_link(controller: :snippets) do
- = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
- Snippets
- %li.divider
- %li
- = link_to "Help", help_path, title: 'About GitLab CE'
diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml
deleted file mode 100644
index c7dabbd8237..00000000000
--- a/app/views/layouts/nav/_new_group_sidebar.html.haml
+++ /dev/null
@@ -1,89 +0,0 @@
-.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
- .context-header
- = link_to group_path(@group), title: @group.name do
- .avatar-container.s40.group-avatar
- = image_tag group_icon(@group), class: "avatar s40 avatar-tile"
- .group-title
- = @group.name
- %ul.sidebar-top-level-items
- = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
- = link_to group_path(@group), title: 'Group overview' do
- .nav-icon-container
- = custom_icon('project')
- %span.nav-item-name
- Overview
-
- %ul.sidebar-sub-level-items
- = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
- = link_to group_path(@group), title: 'Group details' do
- %span
- Details
-
- = nav_link(path: 'groups#activity') do
- = link_to activity_group_path(@group), title: 'Activity' do
- %span
- Activity
-
- = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
- = link_to issues_group_path(@group), title: 'Issues' do
- .nav-icon-container
- = custom_icon('issues')
- %span.nav-item-name
- - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
- Issues
- %span.badge.count= number_with_delimiter(issues.count)
-
- %ul.sidebar-sub-level-items
- = 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
-
- = nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
- .nav-icon-container
- = custom_icon('mr_bold')
- %span.nav-item-name
- - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
- Merge Requests
- %span.badge.count= number_with_delimiter(merge_requests.count)
- = nav_link(path: 'group_members#index') do
- = link_to group_group_members_path(@group), title: 'Members' do
- .nav-icon-container
- = custom_icon('members')
- %span.nav-item-name
- Members
- - if current_user && can?(current_user, :admin_group, @group)
- = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
- = link_to edit_group_path(@group), title: 'Settings' do
- .nav-icon-container
- = custom_icon('settings')
- %span.nav-item-name
- Settings
- %ul.sidebar-sub-level-items
- = 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: 'CI / CD' do
- %span
- CI / CD
-
- = render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml
deleted file mode 100644
index edae009a28e..00000000000
--- a/app/views/layouts/nav/_new_profile_sidebar.html.haml
+++ /dev/null
@@ -1,84 +0,0 @@
-.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
- .context-header
- = link_to profile_path, title: 'Profile Settings' do
- .avatar-container.s40.settings-avatar
- = icon('user')
- .project-title User Settings
- %ul.sidebar-top-level-items
- = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
- = link_to profile_path, title: 'Profile Settings' do
- .nav-icon-container
- = custom_icon('profile')
- %span.nav-item-name
- Profile
- = nav_link(controller: [:accounts, :two_factor_auths]) do
- = link_to profile_account_path, title: 'Account' do
- .nav-icon-container
- = custom_icon('account')
- %span.nav-item-name
- Account
- - if current_application_settings.user_oauth_applications?
- = nav_link(controller: 'oauth/applications') do
- = link_to applications_profile_path, title: 'Applications' do
- .nav-icon-container
- = custom_icon('applications')
- %span.nav-item-name
- Applications
- = nav_link(controller: :chat_names) do
- = link_to profile_chat_names_path, title: 'Chat' do
- .nav-icon-container
- = custom_icon('chat')
- %span.nav-item-name
- Chat
- = nav_link(controller: :personal_access_tokens) do
- = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
- .nav-icon-container
- = custom_icon('access_tokens')
- %span.nav-item-name
- Access Tokens
- = nav_link(controller: :emails) do
- = link_to profile_emails_path, title: 'Emails' do
- .nav-icon-container
- = custom_icon('emails')
- %span.nav-item-name
- Emails
- - unless current_user.ldap_user?
- = nav_link(controller: :passwords) do
- = link_to edit_profile_password_path, title: 'Password' do
- .nav-icon-container
- = custom_icon('lock')
- %span.nav-item-name
- Password
- = nav_link(controller: :notifications) do
- = link_to profile_notifications_path, title: 'Notifications' do
- .nav-icon-container
- = custom_icon('notifications')
- %span.nav-item-name
- Notifications
-
- = nav_link(controller: :keys) do
- = link_to profile_keys_path, title: 'SSH Keys' do
- .nav-icon-container
- = custom_icon('key')
- %span.nav-item-name
- SSH Keys
- = nav_link(controller: :gpg_keys) do
- = link_to profile_gpg_keys_path, title: 'GPG Keys' do
- .nav-icon-container
- = custom_icon('key_2')
- %span.nav-item-name
- GPG Keys
- = nav_link(controller: :preferences) do
- = link_to profile_preferences_path, title: 'Preferences' do
- .nav-icon-container
- = custom_icon('preferences')
- %span.nav-item-name
- Preferences
- = nav_link(path: 'profiles#audit_log') do
- = link_to audit_log_profile_path, title: 'Authentication log' do
- .nav-icon-container
- = custom_icon('authentication_log')
- %span.nav-item-name
- Authentication log
-
- = render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml
deleted file mode 100644
index e0477c29ebe..00000000000
--- a/app/views/layouts/nav/_new_project_sidebar.html.haml
+++ /dev/null
@@ -1,261 +0,0 @@
-.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
- - can_edit = can?(current_user, :admin_project, @project)
- .context-header
- = link_to project_path(@project), title: @project.name do
- .avatar-container.s40.project-avatar
- = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile')
- .project-title
- = @project.name
- %ul.sidebar-top-level-items
- = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
- = link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do
- .nav-icon-container
- = custom_icon('project')
- %span.nav-item-name
- Overview
-
- %ul.sidebar-sub-level-items
- = nav_link(path: 'projects#show') do
- = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
- %span= _('Details')
-
- = 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')
-
- - 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
- = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do
- .nav-icon-container
- = custom_icon('doc_text')
- %span.nav-item-name
- Repository
-
- %ul.sidebar-sub-level-items
- = 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') }
-
- - if project_nav_tab? :container_registry
- = nav_link(controller: %w[projects/registry/repositories]) do
- = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
- .nav-icon-container
- = custom_icon('container_registry')
- %span.nav-item-name
- Registry
-
- - if project_nav_tab? :issues
- = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
- = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do
- .nav-icon-container
- = custom_icon('issues')
- %span.nav-item-name
- Issues
- - if @project.issues_enabled?
- %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
-
- %ul.sidebar-sub-level-items
- = 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
-
- = nav_link(controller: :labels) do
- = link_to project_labels_path(@project), title: 'Labels' do
- %span
- Labels
-
- = nav_link(controller: :milestones) do
- = link_to project_milestones_path(@project), title: 'Milestones' do
- %span
- Milestones
-
- - if project_nav_tab? :merge_requests
- = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
- = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
- .nav-icon-container
- = custom_icon('mr_bold')
- %span.nav-item-name
- Merge Requests
- %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
-
- - if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do
- = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do
- .nav-icon-container
- = custom_icon('pipeline')
- %span.nav-item-name
- CI / CD
-
- %ul.sidebar-sub-level-items
- - 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
-
- - if project_nav_tab? :wiki
- = nav_link(controller: :wikis) do
- = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
- .nav-icon-container
- = custom_icon('wiki')
- %span.nav-item-name
- Wiki
-
- - if project_nav_tab? :snippets
- = nav_link(controller: :snippets) do
- = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do
- .nav-icon-container
- = custom_icon('snippets')
- %span.nav-item-name
- Snippets
-
- - 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
- = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
- .nav-icon-container
- = custom_icon('settings')
- %span.nav-item-name
- Settings
-
- %ul.sidebar-sub-level-items
- - can_edit = can?(current_user, :admin_project, @project)
- - if can_edit
- = nav_link(path: %w[projects#edit]) do
- = link_to edit_project_path(@project), title: 'General' do
- %span
- General
- = nav_link(controller: :project_members) do
- = link_to project_project_members_path(@project), title: 'Members' do
- %span
- Members
- - 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: 'CI / CD' do
- %span
- CI / CD
- - if Gitlab.config.pages.enabled
- = nav_link(controller: :pages) do
- = link_to project_pages_path(@project), title: 'Pages' do
- %span
- Pages
-
- - else
- = 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')
- %span.nav-item-name
- Members
-
- = render 'shared/sidebar_toggle_button'
-
- -# Shortcut to Project > Activity
- %li.hidden
- = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
- %span
- Activity
-
- -# Shortcut to Repository > Graph (formerly, Network)
- - if project_nav_tab? :network
- %li.hidden
- = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do
- Graph
-
- -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs")
- - unless @project.empty_repo?
- %li.hidden
- = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do
- Charts
-
- -# Shortcut to Issues > New Issue
- %li.hidden
- = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
- Create a new issue
-
- -# Shortcut to Pipelines > Jobs
- - if project_nav_tab? :builds
- %li.hidden
- = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
- Jobs
-
- -# Shortcut to commits page
- - if project_nav_tab? :commits
- %li.hidden
- = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
- Commits
-
- -# Shortcut to issue boards
- %li.hidden
- = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
deleted file mode 100644
index 26d9640e98a..00000000000
--- a/app/views/layouts/nav/_profile.html.haml
+++ /dev/null
@@ -1,57 +0,0 @@
-.scrolling-tabs-container
- .fade-left
- = icon('angle-left')
- .fade-right
- = icon('angle-right')
- %ul.nav-links.scrolling-tabs
- = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
- = link_to profile_path, title: 'Profile Settings' do
- %span
- Profile
- = nav_link(controller: [:accounts, :two_factor_auths]) do
- = link_to profile_account_path, title: 'Account' do
- %span
- Account
- - if current_application_settings.user_oauth_applications?
- = nav_link(controller: 'oauth/applications') do
- = link_to applications_profile_path, title: 'Applications' do
- %span
- Applications
- = nav_link(controller: :chat_names) do
- = link_to profile_chat_names_path, title: 'Chat' do
- %span
- Chat
- = nav_link(controller: :personal_access_tokens) do
- = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
- %span
- Access Tokens
- = nav_link(controller: :emails) do
- = link_to profile_emails_path, title: 'Emails' do
- %span
- Emails
- - if current_user.allow_password_authentication?
- = nav_link(controller: :passwords) do
- = link_to edit_profile_password_path, title: 'Password' do
- %span
- Password
- = nav_link(controller: :notifications) do
- = link_to profile_notifications_path, title: 'Notifications' do
- %span
- Notifications
-
- = nav_link(controller: :keys) do
- = link_to profile_keys_path, title: 'SSH Keys' do
- %span
- SSH Keys
- = nav_link(controller: :gpg_keys) do
- = link_to profile_gpg_keys_path, title: 'GPG Keys' do
- %span
- GPG Keys
- = nav_link(controller: :preferences) do
- = link_to profile_preferences_path, title: 'Preferences' do
- %span
- Preferences
- = nav_link(path: 'profiles#audit_log') do
- = link_to audit_log_profile_path, title: 'Authentication log' do
- %span
- Authentication log
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
deleted file mode 100644
index 924cd2e9681..00000000000
--- a/app/views/layouts/nav/_project.html.haml
+++ /dev/null
@@ -1,109 +0,0 @@
-- can_edit = can?(current_user, :admin_project, @project)
-.scrolling-tabs-container{ class: nav_control_class }
- .fade-left
- = icon('angle-left')
- .fade-right
- = icon('angle-right')
- %ul.nav-links.scrolling-tabs
- = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
- = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
- %span
- Project
-
- - 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
- = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do
- %span
- Repository
-
- - if project_nav_tab? :container_registry
- = nav_link(controller: %w[projects/registry/repositories]) do
- = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do
- %span
- Registry
-
- - if project_nav_tab? :issues
- = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
- = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do
- %span
- Issues
- - if @project.issues_enabled?
- %span.badge.count.issue_counter= number_with_delimiter(issuables_count_for_state(:issues, :opened, finder: IssuesFinder.new(current_user, project_id: @project.id)))
-
- - if project_nav_tab? :merge_requests
- - controllers = [:merge_requests, 'projects/merge_requests/conflicts']
- - controllers.push(:merge_requests, :labels, :milestones) unless @project.issues_enabled?
- = nav_link(controller: controllers) do
- = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
- %span
- Merge Requests
- %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(issuables_count_for_state(:merge_requests, :opened, finder: MergeRequestsFinder.new(current_user, project_id: @project.id)))
-
- - if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do
- = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
- %span
- Pipelines
-
- - if project_nav_tab? :wiki
- = nav_link(controller: :wikis) do
- = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
- %span
- Wiki
-
- - if project_nav_tab? :snippets
- = nav_link(controller: :snippets) do
- = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do
- %span
- Snippets
-
- - if project_nav_tab? :project_members
- = nav_link(controller: :project_members) do
- = link_to project_project_members_path(@project), title: 'Members', class: 'shortcuts-members' do
- %span
- Members
-
- - if project_nav_tab? :settings
- = nav_link(path: %w[projects#edit members#show integrations#show services#edit repository#show ci_cd#show pages#show]) do
- = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
- %span
- Settings
-
- -# Shortcut to Project > Activity
- %li.hidden
- = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
- %span
- Activity
-
- -# Shortcut to Repository > Graph (formerly, Network)
- - if project_nav_tab? :network
- %li.hidden
- = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do
- Graph
-
- -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs")
- - unless @project.empty_repo?
- %li.hidden
- = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do
- Charts
-
- -# Shortcut to Issues > New Issue
- %li.hidden
- = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
- Create a new issue
-
- -# Shortcut to Pipelines > Jobs
- - if project_nav_tab? :builds
- %li.hidden
- = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
- Jobs
-
- -# Shortcut to commits page
- - if project_nav_tab? :commits
- %li.hidden
- = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
- Commits
-
- -# Shortcut to issue boards
- %li.hidden
- = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
new file mode 100644
index 00000000000..28022eebb19
--- /dev/null
+++ b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml
@@ -0,0 +1,11 @@
+- dropdown_location = local_assigns.fetch(:location, nil)
+- button_tooltip = local_assigns.fetch(:title, _("Show parent pages"))
+- if defined?(@breadcrumb_dropdown_links) && @breadcrumb_dropdown_links.key?(dropdown_location)
+ %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")
+ .dropdown-menu
+ %ul
+ - @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index|
+ %li{ style: "text-indent: #{[index * 16, 60].min}px;" }= link
diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml
new file mode 100644
index 00000000000..a7370180bf6
--- /dev/null
+++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml
@@ -0,0 +1,15 @@
+- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: @project.web_url, avatar_url: @project.avatar_url } if @project&.persisted?
+.projects-dropdown-container
+ .project-dropdown-sidebar
+ %ul
+ = nav_link(path: 'dashboard/projects#index') do
+ = link_to dashboard_projects_path do
+ = _('Your projects')
+ = nav_link(path: 'projects#starred') do
+ = link_to starred_dashboard_projects_path do
+ = _('Starred projects')
+ = nav_link(path: 'projects#trending') do
+ = link_to explore_root_path do
+ = _('Explore projects')
+ .project-dropdown-content
+ #js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } }
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
new file mode 100644
index 00000000000..8ab2b686f86
--- /dev/null
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -0,0 +1,212 @@
+.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
+ .nav-sidebar-inner-scroll
+ .context-header
+ = link_to admin_root_path, title: 'Admin Overview' do
+ .avatar-container.s40.settings-avatar
+ = icon('wrench')
+ .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-icon-container
+ = custom_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
+ = link_to admin_root_path do
+ %strong.fly-out-top-item-name
+ #{ _('Overview') }
+ %li.divider.fly-out-top-item
+ = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
+ = link_to admin_root_path, title: 'Overview' do
+ %span
+ Dashboard
+ = 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: 'jobs#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
+
+ = 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-icon-container
+ = custom_icon('monitoring')
+ %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
+ %strong.fly-out-top-item-name
+ #{ _('Monitoring') }
+ %li.divider.fly-out-top-item
+ = 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
+
+ = nav_link(controller: :broadcast_messages) do
+ = sidebar_link admin_broadcast_messages_path, title: _('Messages') do
+ .nav-icon-container
+ = custom_icon('messages')
+ %span.nav-item-name
+ Messages
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :broadcast_messages, html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_broadcast_messages_path do
+ %strong.fly-out-top-item-name
+ #{ _('Messages') }
+
+ = nav_link(controller: [:hooks, :hook_logs]) do
+ = sidebar_link admin_hooks_path, title: _('Hooks') do
+ .nav-icon-container
+ = custom_icon('system_hooks')
+ %span.nav-item-name
+ System Hooks
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: [:hooks, :hook_logs], html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_hooks_path do
+ %strong.fly-out-top-item-name
+ #{ _('System Hooks') }
+
+ = nav_link(controller: :applications) do
+ = sidebar_link admin_applications_path, title: _('Applications') do
+ .nav-icon-container
+ = custom_icon('applications')
+ %span.nav-item-name
+ Applications
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :applications, html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_applications_path do
+ %strong.fly-out-top-item-name
+ #{ _('Applications') }
+
+ = nav_link(controller: :abuse_reports) do
+ = sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do
+ .nav-icon-container
+ = custom_icon('abuse_reports')
+ %span.nav-item-name
+ Abuse Reports
+ %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_broadcast_messages_path do
+ %strong.fly-out-top-item-name
+ #{ _('Abuse Reports') }
+ %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all))
+
+ - if akismet_enabled?
+ = nav_link(controller: :spam_logs) do
+ = sidebar_link admin_spam_logs_path, title: _("Spam Logs") do
+ .nav-icon-container
+ = custom_icon('spam_logs')
+ %span.nav-item-name
+ Spam Logs
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :spam_logs, html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_spam_logs_path do
+ %strong.fly-out-top-item-name
+ #{ _('Spam Logs') }
+
+ = nav_link(controller: :deploy_keys) do
+ = sidebar_link admin_deploy_keys_path, title: _('Deploy Keys') do
+ .nav-icon-container
+ = custom_icon('key')
+ %span.nav-item-name
+ Deploy Keys
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :deploy_keys, html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_deploy_keys_path do
+ %strong.fly-out-top-item-name
+ #{ _('Deploy Keys') }
+
+ = nav_link(controller: :services) do
+ = sidebar_link admin_application_settings_services_path, title: _('Service Templates') do
+ .nav-icon-container
+ = custom_icon('service_templates')
+ %span.nav-item-name
+ Service Templates
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :services, html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_application_settings_services_path do
+ %strong.fly-out-top-item-name
+ #{ _('Service Templates') }
+
+ = nav_link(controller: :labels) do
+ = sidebar_link admin_labels_path, title: _('Labels') do
+ .nav-icon-container
+ = custom_icon('labels')
+ %span.nav-item-name
+ Labels
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :labels, html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_labels_path do
+ %strong.fly-out-top-item-name
+ #{ _('Labels') }
+
+ = nav_link(controller: :appearances) do
+ = sidebar_link admin_appearances_path, title: _('Appearances') do
+ .nav-icon-container
+ = custom_icon('appearance')
+ %span.nav-item-name
+ Appearance
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :appearances, html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_appearances_path do
+ %strong.fly-out-top-item-name
+ #{ _('Appearance') }
+
+ = nav_link(controller: :application_settings) do
+ = sidebar_link admin_application_settings_path, title: _('Settings') do
+ .nav-icon-container
+ = custom_icon('settings')
+ %span.nav-item-name
+ Settings
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :application_settings, html_options: { class: "fly-out-top-item" } ) do
+ = link_to admin_application_settings_path do
+ %strong.fly-out-top-item-name
+ #{ _('Settings') }
+
+ = render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
new file mode 100644
index 00000000000..e01dfa7c854
--- /dev/null
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -0,0 +1,118 @@
+- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
+- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
+
+.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
+ .nav-sidebar-inner-scroll
+ .context-header
+ = link_to group_path(@group), title: @group.name do
+ .avatar-container.s40.group-avatar
+ = image_tag group_icon(@group), class: "avatar s40 avatar-tile"
+ .sidebar-context-title
+ = @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
+ .nav-icon-container
+ = custom_icon('project')
+ %span.nav-item-name
+ Overview
+
+ %ul.sidebar-sub-level-items
+ = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: "fly-out-top-item" } ) do
+ = link_to group_path(@group) do
+ %strong.fly-out-top-item-name
+ #{ _('Overview') }
+ %li.divider.fly-out-top-item
+ = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
+ = link_to group_path(@group), title: 'Group details' do
+ %span
+ Details
+
+ = nav_link(path: 'groups#activity') do
+ = link_to activity_group_path(@group), title: 'Activity' do
+ %span
+ Activity
+
+ = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
+ = sidebar_link issues_group_path(@group), title: _('Issues') do
+ .nav-icon-container
+ = custom_icon('issues')
+ %span.nav-item-name
+ Issues
+ %span.badge.count= number_with_delimiter(issues.count)
+
+ %ul.sidebar-sub-level-items
+ = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index'], html_options: { class: "fly-out-top-item" } ) do
+ = link_to issues_group_path(@group) do
+ %strong.fly-out-top-item-name
+ #{ _('Issues') }
+ %span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues.count)
+ %li.divider.fly-out-top-item
+ = 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
+
+ = nav_link(path: 'groups#merge_requests') do
+ = sidebar_link merge_requests_group_path(@group), title: _('Merge Requests') do
+ .nav-icon-container
+ = custom_icon('mr_bold')
+ %span.nav-item-name
+ Merge Requests
+ %span.badge.count= number_with_delimiter(merge_requests.count)
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
+ = link_to merge_requests_group_path(@group) do
+ %strong.fly-out-top-item-name
+ #{ _('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
+ .nav-icon-container
+ = custom_icon('members')
+ %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
+ %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
+ .nav-icon-container
+ = custom_icon('settings')
+ %span.nav-item-name
+ Settings
+ %ul.sidebar-sub-level-items
+ = nav_link(path: %w[groups#projects groups#edit ci_cd#show], html_options: { class: "fly-out-top-item" } ) do
+ = link_to edit_group_path(@group) do
+ %strong.fly-out-top-item-name
+ #{ _('Settings') }
+ %li.divider.fly-out-top-item
+ = 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: 'CI / CD' do
+ %span
+ CI / CD
+
+ = render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
new file mode 100644
index 00000000000..4c26d107ea7
--- /dev/null
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -0,0 +1,144 @@
+.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
+ .nav-sidebar-inner-scroll
+ .context-header
+ = link_to profile_path, title: 'Profile Settings' do
+ .avatar-container.s40.settings-avatar
+ = icon('user')
+ .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
+ .nav-icon-container
+ = custom_icon('profile')
+ %span.nav-item-name
+ Profile
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(path: 'profiles#show', html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_path do
+ %strong.fly-out-top-item-name
+ #{ _('Profile') }
+ = nav_link(controller: [:accounts, :two_factor_auths]) do
+ = sidebar_link profile_account_path, title: _('Account') do
+ .nav-icon-container
+ = custom_icon('account')
+ %span.nav-item-name
+ Account
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: [:accounts, :two_factor_auths], html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_account_path do
+ %strong.fly-out-top-item-name
+ #{ _('Account') }
+ - if current_application_settings.user_oauth_applications?
+ = nav_link(controller: 'oauth/applications') do
+ = sidebar_link applications_profile_path, title: _('Applications') do
+ .nav-icon-container
+ = custom_icon('applications')
+ %span.nav-item-name
+ Applications
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" } ) do
+ = link_to applications_profile_path do
+ %strong.fly-out-top-item-name
+ #{ _('Applications') }
+ = nav_link(controller: :chat_names) do
+ = sidebar_link profile_chat_names_path, title: _('Chat') do
+ .nav-icon-container
+ = custom_icon('chat')
+ %span.nav-item-name
+ Chat
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :chat_names, html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_chat_names_path do
+ %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
+ .nav-icon-container
+ = custom_icon('access_tokens')
+ %span.nav-item-name
+ Access Tokens
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_personal_access_tokens_path do
+ %strong.fly-out-top-item-name
+ #{ _('Access Tokens') }
+ = nav_link(controller: :emails) do
+ = sidebar_link profile_emails_path, title: _('Emails') do
+ .nav-icon-container
+ = custom_icon('emails')
+ %span.nav-item-name
+ Emails
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :emails, html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_emails_path do
+ %strong.fly-out-top-item-name
+ #{ _('Emails') }
+ - unless current_user.ldap_user?
+ = nav_link(controller: :passwords) do
+ = sidebar_link edit_profile_password_path, title: _('Password') do
+ .nav-icon-container
+ = custom_icon('lock')
+ %span.nav-item-name
+ Password
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :passwords, html_options: { class: "fly-out-top-item" } ) do
+ = link_to edit_profile_password_path do
+ %strong.fly-out-top-item-name
+ #{ _('Password') }
+ = nav_link(controller: :notifications) do
+ = sidebar_link profile_notifications_path, title: _('Notifications') do
+ .nav-icon-container
+ = custom_icon('notifications')
+ %span.nav-item-name
+ Notifications
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :notifications, html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_notifications_path do
+ %strong.fly-out-top-item-name
+ #{ _('Notifications') }
+ = nav_link(controller: :keys) do
+ = sidebar_link profile_keys_path, title: _('SSH Keys') do
+ .nav-icon-container
+ = custom_icon('key')
+ %span.nav-item-name
+ SSH Keys
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :keys, html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_keys_path do
+ %strong.fly-out-top-item-name
+ #{ _('SSH Keys') }
+ = nav_link(controller: :gpg_keys) do
+ = sidebar_link profile_gpg_keys_path, title: _('GPG Keys') do
+ .nav-icon-container
+ = custom_icon('key_2')
+ %span.nav-item-name
+ GPG Keys
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :gpg_keys, html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_gpg_keys_path do
+ %strong.fly-out-top-item-name
+ #{ _('GPG Keys') }
+ = nav_link(controller: :preferences) do
+ = sidebar_link profile_preferences_path, title: _('Preferences') do
+ .nav-icon-container
+ = custom_icon('preferences')
+ %span.nav-item-name
+ Preferences
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :preferences, html_options: { class: "fly-out-top-item" } ) do
+ = link_to profile_preferences_path do
+ %strong.fly-out-top-item-name
+ #{ _('Preferences') }
+ = nav_link(path: 'profiles#audit_log') do
+ = sidebar_link audit_log_profile_path, title: _('Authentication log') do
+ .nav-icon-container
+ = custom_icon('authentication_log')
+ %span.nav-item-name
+ Authentication log
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(path: 'profiles#audit_log', html_options: { class: "fly-out-top-item" } ) do
+ = link_to audit_log_profile_path do
+ %strong.fly-out-top-item-name
+ #{ _('Authentication Log') }
+
+ = render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
new file mode 100644
index 00000000000..27fadc1d952
--- /dev/null
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -0,0 +1,323 @@
+.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
+ .nav-sidebar-inner-scroll
+ - can_edit = can?(current_user, :admin_project, @project)
+ .context-header
+ = link_to project_path(@project), title: @project.name do
+ .avatar-container.s40.project-avatar
+ = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile')
+ .sidebar-context-title
+ = @project.name
+ %ul.sidebar-top-level-items
+ = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
+ = sidebar_link project_path(@project), title: _('Project overview'), css: 'shortcuts-project' do
+ .nav-icon-container
+ = custom_icon('project')
+ %span.nav-item-name
+ Overview
+
+ %ul.sidebar-sub-level-items
+ = nav_link(path: 'projects#show', html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_path(@project) do
+ %strong.fly-out-top-item-name
+ #{ _('Overview') }
+ %li.divider.fly-out-top-item
+ = nav_link(path: 'projects#show') do
+ = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do
+ %span= _('Details')
+
+ = 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')
+
+ - 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
+ .nav-icon-container
+ = custom_icon('doc_text')
+ %span.nav-item-name
+ Repository
+
+ %ul.sidebar-sub-level-items
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network), html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_tree_path(@project) do
+ %strong.fly-out-top-item-name
+ #{ _('Repository') }
+ %li.divider.fly-out-top-item
+ = 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') }
+
+ - 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
+ .nav-icon-container
+ = custom_icon('container_registry')
+ %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
+ .nav-icon-container
+ = custom_icon('issues')
+ %span.nav-item-name
+ Issues
+ - if @project.issues_enabled?
+ %span.badge.count.issue_counter
+ = number_with_delimiter(@project.open_issues_count)
+
+ %ul.sidebar-sub-level-items
+ = nav_link(controller: :issues, html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_issues_path(@project) do
+ %strong.fly-out-top-item-name
+ #{ _('Issues') }
+ - if @project.issues_enabled?
+ %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
+ = 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
+ .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')
+
+ = nav_link(controller: :labels) do
+ = link_to project_labels_path(@project), title: 'Labels' do
+ %span
+ Labels
+
+ = nav_link(controller: :milestones) do
+ = link_to project_milestones_path(@project), title: 'Milestones' do
+ %span
+ Milestones
+
+ - 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
+ .nav-icon-container
+ = custom_icon('mr_bold')
+ %span.nav-item-name
+ Merge Requests
+ %span.badge.count.merge_counter.js-merge-counter
+ = number_with_delimiter(@project.open_merge_requests_count)
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :merge_requests, html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_merge_requests_path(@project) do
+ %strong.fly-out-top-item-name
+ #{ _('Merge Requests') }
+ %span.badge.count.merge_counter.js-merge-counter.fly-out-badge
+ = 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-icon-container
+ = custom_icon('pipeline')
+ %span.nav-item-name
+ CI / CD
+
+ %ul.sidebar-sub-level-items
+ = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts], html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_pipelines_path(@project) do
+ %strong.fly-out-top-item-name
+ #{ _('CI / CD') }
+ %li.divider.fly-out-top-item
+ - 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
+
+ - if project_nav_tab? :wiki
+ = nav_link(controller: :wikis) do
+ = sidebar_link get_project_wiki_path(@project), title: _('Wiki'), css: 'shortcuts-wiki' do
+ .nav-icon-container
+ = custom_icon('wiki')
+ %span.nav-item-name
+ Wiki
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do
+ = link_to get_project_wiki_path(@project) do
+ %strong.fly-out-top-item-name
+ #{ _('Wiki') }
+
+ - if project_nav_tab? :snippets
+ = nav_link(controller: :snippets) do
+ = sidebar_link project_snippets_path(@project), title: _('Snippets'), css: 'shortcuts-snippets' do
+ .nav-icon-container
+ = custom_icon('snippets')
+ %span.nav-item-name
+ Snippets
+ %ul.sidebar-sub-level-items.is-fly-out-only
+ = nav_link(controller: :snippets, html_options: { class: "fly-out-top-item" } ) do
+ = link_to project_snippets_path(@project) do
+ %strong.fly-out-top-item-name
+ #{ _('Snippets') }
+
+ - 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
+ .nav-icon-container
+ = custom_icon('settings')
+ %span.nav-item-name
+ Settings
+
+ %ul.sidebar-sub-level-items
+ - can_edit = can?(current_user, :admin_project, @project)
+ - if can_edit
+ = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show], html_options: { class: "fly-out-top-item" } ) do
+ = link_to edit_project_path(@project) do
+ %strong.fly-out-top-item-name
+ #{ _('Settings') }
+ %li.divider.fly-out-top-item
+ = nav_link(path: %w[projects#edit]) do
+ = link_to edit_project_path(@project), title: 'General' do
+ %span
+ General
+ = nav_link(controller: :project_members) do
+ = link_to project_project_members_path(@project), title: 'Members' do
+ %span
+ Members
+ - 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: 'CI / CD' do
+ %span
+ CI / CD
+ - if @project.pages_available?
+ = nav_link(controller: :pages) do
+ = link_to project_pages_path(@project), title: 'Pages' do
+ %span
+ Pages
+
+ - else
+ = 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')
+ %span.nav-item-name
+ Members
+
+ = render 'shared/sidebar_toggle_button'
+
+ -# Shortcut to Project > Activity
+ %li.hidden
+ = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
+ %span
+ Activity
+
+ -# Shortcut to Repository > Graph (formerly, Network)
+ - if project_nav_tab? :network
+ %li.hidden
+ = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do
+ Graph
+
+ -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs")
+ - unless @project.empty_repo?
+ %li.hidden
+ = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do
+ Charts
+
+ -# Shortcut to Issues > New Issue
+ %li.hidden
+ = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do
+ Create a new issue
+
+ -# Shortcut to Pipelines > Jobs
+ - if project_nav_tab? :builds
+ %li.hidden
+ = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
+ Jobs
+
+ -# Shortcut to commits page
+ - if project_nav_tab? :commits
+ %li.hidden
+ = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
+ Commits
+
+ -# Shortcut to issue boards
+ %li.hidden
+ = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards'
diff --git a/app/views/layouts/oauth_error.html.haml b/app/views/layouts/oauth_error.html.haml
index 34bcd2a8b3a..03b387f8181 100644
--- a/app/views/layouts/oauth_error.html.haml
+++ b/app/views/layouts/oauth_error.html.haml
@@ -19,7 +19,7 @@
h3 {
color: #456;
font-size: 22px;
- font-weight: bold;
+ font-weight: 600;
margin-bottom: 6px;
}
diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml
index c365839e605..67aa05b655c 100644
--- a/app/views/layouts/profile.html.haml
+++ b/app/views/layouts/profile.html.haml
@@ -1,10 +1,7 @@
- page_title "User Settings"
- header_title "User Settings", profile_path unless header_title
- sidebar "dashboard"
-- if show_new_nav?
- - nav "new_profile_sidebar"
- - @new_sidebar = true
-- else
- - nav "profile"
+- nav "profile"
+- @left_sidebar = true
= render template: "layouts/application"
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 54d56e9b873..6b847fb4b7c 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -1,11 +1,8 @@
- page_title @project.name_with_namespace
- page_description @project.description unless page_description
- header_title project_title(@project) unless header_title
-- if show_new_nav?
- - nav "new_project_sidebar"
- - @new_sidebar = true
-- else
- - nav "project"
+- nav "project"
+- @left_sidebar = true
- content_for :project_javascripts do
- project = @target_project || @project
@@ -14,12 +11,4 @@
:javascript
window.uploads_path = "#{project_uploads_path(project)}";
-- content_for :header_content do
- .js-dropdown-menu-projects
- .dropdown-menu.dropdown-select.dropdown-menu-projects
- = dropdown_title("Go to a project")
- = dropdown_filter("Search your projects")
- = dropdown_content
- = dropdown_loading
-
= render template: "layouts/application"
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index ed079ed7dfb..5d778d67ae7 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -92,25 +92,24 @@
Update username
%hr
-- if signup_enabled?
- .row.prepend-top-default
- .col-lg-4.profile-settings-sidebar
- %h4.prepend-top-0.danger-title
- Remove account
- .col-lg-8
- - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
+.row.prepend-top-default
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0.danger-title
+ Remove account
+ .col-lg-8
+ - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
+ %p
+ 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"
+ - else
+ - if @user.solo_owned_groups.present?
%p
- 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"
+ 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.
- else
- - if @user.solo_owned_groups.present?
- %p
- 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.
- - else
- %p
- You don't have access to delete this user.
+ %p
+ You don't have access to delete this user.
.append-bottom-default
diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml
index 8331daeeb75..8dbb8aef31b 100644
--- a/app/views/profiles/gpg_keys/index.html.haml
+++ b/app/views/profiles/gpg_keys/index.html.haml
@@ -12,7 +12,7 @@
Add a GPG key
%p.profile-settings-content
Before you can add a GPG key you need to
- = link_to 'generate it.', help_page_path('workflow/gpg_signed_commits/index.md')
+ = link_to 'generate it.', help_page_path('user/project/repository/gpg_signed_commits/index.md')
= render 'form'
%hr
%h5
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index d2a60ac2867..103446243e5 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -1,6 +1,12 @@
%li.key-list-item
.pull-left.append-right-10
- = icon 'key', class: "settings-list-icon hidden-xs"
+ - if key.valid?
+ = icon 'key', class: 'settings-list-icon hidden-xs'
+ - else
+ = icon 'exclamation-triangle', class: 'settings-list-icon hidden-xs has-tooltip',
+ title: key.errors.full_messages.join(', ')
+
+
.key-list-item-info
= link_to path_to_key(key, is_admin), class: "title" do
= key.title
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index d44603c638c..77521417f47 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -16,6 +16,7 @@
%strong= @key.last_used_at.try(:to_s, :medium) || 'N/A'
.col-md-8
+ = form_errors(@key, type: 'key') unless @key.valid?
%p
%span.light Fingerprint:
%code.key-fingerprint= @key.fingerprint
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index 985bb79508f..c606b5a1e6c 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -1,3 +1,4 @@
+- breadcrumb_title "Edit Password"
- page_title "Password"
- @content_class = "limit-container-width" unless fluid_layout
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 2216708d354..06bb72b9f0d 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -1,3 +1,4 @@
+- breadcrumb_title "Access Tokens"
- page_title "Personal Access Tokens"
- @content_class = "limit-container-width" unless fluid_layout
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 9bd8bf91d1c..2b72eeab8d6 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -3,6 +3,26 @@
= render 'profiles/head'
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
+ .col-lg-4.application-theme
+ %h4.prepend-top-0
+ GitLab navigation theme
+ %p Customize the appearance of the application header and navigation sidebar.
+ .col-lg-8.application-theme
+ - Gitlab::Themes.each do |theme|
+ = label_tag do
+ .preview{ class: theme.name.downcase }
+ .preview-row
+ .quadrant.one
+ .quadrant.two
+ .preview-row
+ .quadrant.three
+ .quadrant.four
+ = f.radio_button :theme_id, theme.id
+ = theme.name
+
+ .col-sm-12
+ %hr
+
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Syntax highlighting theme
@@ -18,50 +38,6 @@
= scheme.name
.col-sm-12
%hr
- %h3#experimental-features Experimental features
- %hr
- .col-lg-4.profile-settings-sidebar#new-navigation
- %h4.prepend-top-0
- New Navigation
- %p
- This setting allows you to turn on or off the new upcoming navigation concept.
- .col-lg-8.syntax-theme
- .nav-wip
- %p
- The new navigation is currently a work-in-progress concept and is currently only usable on wide-screens. There are a number of improvements that we are working on in order to further refine our navigation.
- %p
- %a{ href: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/32794', target: 'blank' } Learn more
- about the improvements that are coming soon!
- = label_tag do
- .preview= image_tag "old_nav.png"
- %input.js-experiment-feature-toggle{ type: "radio", value: "false", name: "new_nav", checked: !show_new_nav? }
- Old
- = label_tag do
- .preview= image_tag "new_nav.png"
- %input.js-experiment-feature-toggle{ type: "radio", value: "true", name: "new_nav", checked: show_new_nav? }
- New
- .col-sm-12
- %hr
- .col-lg-4.profile-settings-sidebar#new-repository
- %h4.prepend-top-0
- New Repository
- %p
- This setting allows you to turn on or off the new upcoming repository concept.
- .col-lg-8.syntax-theme
- .nav-wip
- %p
- The new repository is currently a work-in-progress concept and only usable on wide-screens. There are a number of improvements that we are working on in order to further refine the repository view.
- %p
- %a{ href: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/31890', target: 'blank' } Learn more
- about the improvements that are coming soon!
- = label_tag do
- .preview= image_tag "old_repo.png"
- %input.js-experiment-feature-toggle{ type: "radio", value: "false", name: "new_repo", checked: !show_new_repo? }
- Old
- = label_tag do
- .preview= image_tag "new_repo.png"
- %input.js-experiment-feature-toggle{ type: "radio", value: "true", name: "new_repo", checked: show_new_repo? }
- New
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
index 431ab9d052b..8966dd3fd86 100644
--- a/app/views/profiles/preferences/update.js.erb
+++ b/app/views/profiles/preferences/update.js.erb
@@ -1,3 +1,7 @@
+// Remove body class for any previous theme, re-add current one
+$('body').removeClass('<%= Gitlab::Themes.body_classes %>')
+$('body').addClass('<%= user_application_theme %>')
+
// Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper .container-fluid').removeClass('container-limited')
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index a8ae0b92334..79f334176a5 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,8 +1,8 @@
-- breadcrumb_title "Profile"
+- breadcrumb_title "Edit Profile"
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
-= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default' }, authenticity_token: true do |f|
+= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f|
= form_errors(@user)
.row
@@ -45,12 +45,15 @@
Some options are unavailable for LDAP accounts
.col-lg-8
.row
- = f.text_field :name, required: true, wrapper: { class: 'col-md-9' },
- help: 'Enter your name, so people you know can recognize you.'
+ - if @user.read_only_attribute?(:name)
+ = f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' },
+ help: "Your name was automatically set based on your #{ attribute_provider_label(:name) } account, so people you know can recognize you."
+ - else
+ = f.text_field :name, required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you."
= f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' }
- - if @user.external_email?
- = f.text_field :email, required: true, readonly: true, help: "Your email address was automatically set based on your #{email_provider_label} account."
+ - if @user.read_only_attribute?(:email)
+ = f.text_field :email, required: true, readonly: true, help: "Your email address was automatically set based on your #{ attribute_provider_label(:email) } account."
- else
= f.text_field :email, required: true, value: (@user.email unless @user.temp_oauth_email?),
help: user_email_help_text(@user)
@@ -64,7 +67,10 @@
= f.text_field :linkedin
= f.text_field :twitter
= f.text_field :website_url, label: 'Website'
- = f.text_field :location
+ - if @user.read_only_attribute?(:location)
+ = f.text_field :location, readonly: true, help: "Your location was automatically set based on your #{ attribute_provider_label(:location) } account."
+ - else
+ = f.text_field :location
= f.text_field :organization
= f.text_area :bio, rows: 4, maxlength: 250, help: 'Tell us about yourself in fewer than 250 characters.'
.prepend-top-default.append-bottom-default
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 33e062c1c9c..0b03276efcc 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -1,8 +1,5 @@
- page_title 'Two-Factor Authentication', 'Account'
-- if show_new_nav?
- - add_to_breadcrumbs("Account", profile_account_path)
-- else
- - header_title "Two-Factor Authentication", profile_two_factor_auth_path
+- add_to_breadcrumbs("Account", profile_account_path)
- @content_class = "limit-container-width" unless fluid_layout
= render 'profiles/head'
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index ad63f5e73ae..1f701f2aa1b 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,7 +1,7 @@
%div{ class: container_class }
.nav-block.activity-filter-block.activities
.controls
- = link_to project_path(@project, rss_url_options), title: "Subscribe", class: 'btn rss-btn has-tooltip' do
+ = link_to project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'btn rss-btn has-tooltip' do
= icon('rss')
= render 'shared/event_filter'
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
new file mode 100644
index 00000000000..623d3bc91c6
--- /dev/null
+++ b/app/views/projects/_export.html.haml
@@ -0,0 +1,41 @@
+- return unless current_application_settings.project_export_enabled?
+
+- project = local_assigns.fetch(:project)
+- expanded = Rails.env.test?
+
+%section.settings
+ .settings-header
+ %h4
+ Export project
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
+ .settings-content.no-animate{ class: ('expanded' if expanded) }
+ .bs-callout.bs-callout-info
+ %p.append-bottom-0
+ %p
+ The following items will be exported:
+ %ul
+ %li Project and wiki repositories
+ %li Project uploads
+ %li Project configuration including web hooks and services
+ %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
+ %p
+ The following items will NOT be exported:
+ %ul
+ %li Job traces and artifacts
+ %li LFS objects
+ %li Container registry images
+ %li CI variables
+ %li Any encrypted tokens
+ %p
+ Once the exported file is ready, you will receive a notification email with a download link.
+ - if project.export_project_path
+ = link_to 'Download export', download_export_project_path(project),
+ rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
+ = link_to 'Generate new export', generate_new_export_project_path(project),
+ method: :post, class: "btn btn-default"
+ - else
+ = link_to 'Export project', export_project_path(project),
+ method: :post, class: "btn btn-default"
diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml
index f47d84ef755..0175b519867 100644
--- a/app/views/projects/_flash_messages.html.haml
+++ b/app/views/projects/_flash_messages.html.haml
@@ -1,7 +1,6 @@
- project = local_assigns.fetch(:project)
-- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message
-= content_for flash_message_container do
+= content_for :flash_message do
= render partial: 'deletion_failed', locals: { project: project }
- if current_user && can?(current_user, :download_code, project)
= render 'shared/no_ssh'
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 1a71bfca2e2..56eecece54c 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -3,16 +3,16 @@
.row-content-block.top-block.hidden-xs.white
.event-last-push
.event-last-push-text
- %span You pushed to
+ %span= s_("LastPushEvent|You pushed to")
%strong
= link_to event.ref_name, project_commits_path(event.project, event.ref_name), class: 'ref-name'
- if event.project != @project
- %span at
+ %span= s_("LastPushEvent|at")
%strong= link_to_project event.project
#{time_ago_with_tooltip(event.created_at)}
.pull-right
- = link_to new_mr_path_from_push_event(event), title: "New merge request", class: "btn btn-info btn-sm" do
+ = link_to new_mr_path_from_push_event(event), title: _("New merge request"), class: "btn btn-info btn-sm" do
#{ _('Create merge request') }
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 6e13bf47ff6..71424593f2e 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,12 +1,5 @@
- referenced_users = local_assigns.fetch(:referenced_users, nil)
-- if defined?(@issue) && @issue.confidential?
- %li.confidential-issue-warning
- = confidential_icon(@issue)
- %span This is a confidential issue. Your comment will not be visible to the public.
-- else
- %li.confidential-issue-warning.not-confidential
-
.md-area
.md-header
%ul.nav-links.clearfix
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index 61420fd0fb6..1dd8778f800 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -1,7 +1,7 @@
- form = local_assigns.fetch(:form)
.form-group
- .checkbox.builds-feature
+ .checkbox.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
= form.label :only_allow_merge_if_pipeline_succeeds do
= form.check_box :only_allow_merge_if_pipeline_succeeds
%strong Only allow merge requests to be merged if the pipeline succeeds
@@ -14,6 +14,10 @@
= form.check_box :only_allow_merge_if_all_discussions_are_resolved
%strong Only allow merge requests to be merged if all discussions are resolved
.checkbox
+ = form.label :resolve_outdated_diff_discussions do
+ = form.check_box :resolve_outdated_diff_discussions
+ %strong Automatically resolve merge request diff discussions when they become outdated
+ .checkbox
= form.label :printing_merge_request_link_enabled do
= form.check_box :printing_merge_request_link_enabled
%strong Show link to create/view merge request when pushing from the command line
diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml
index 21baf35f2ac..5638b7da1b0 100644
--- a/app/views/projects/_project_templates.html.haml
+++ b/app/views/projects/_project_templates.html.haml
@@ -1,10 +1,10 @@
.project-templates-buttons.import-buttons{ data: { toggle: "buttons" } }
.btn.blank-option.active
- %input{ type: "radio", autocomplete: "off", name: "project_templates", id: "blank", checked: "true" }
+ %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: "blank", checked: "true", value: "" }
= icon('file-o', class: 'btn-template-icon')
Blank
- Gitlab::ProjectTemplate.all.each do |template|
.btn
- %input{ type: "radio", autocomplete: "off", name: "project_templates", id: template.name }
+ %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name }
= custom_icon(template.logo)
= template.title
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index 9e2688e492e..f80dadb8037 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -1,9 +1,7 @@
- @no_container = true
-- if show_new_nav?
- - add_to_breadcrumbs("Project", project_path(@project))
+- page_title _("Activity")
-- page_title "Activity"
= render "projects/head"
= render 'projects/last_push'
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index a33743c2f57..4cc3218d967 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -1,8 +1,12 @@
+- 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("##{@build.id}", project_jobs_path(@project))
+
.tree-holder
.nav-block
%ul.breadcrumb.repo-breadcrumb
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index c7359d873d9..60ac202bde0 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -22,7 +22,7 @@
= author_avatar(commit, size: 36)
.commit-row-title
%span.item-title.str-truncated-100
- = link_to_gfm commit.title, project_commit_path(@project, commit.id), class: "cdark", title: commit.title
+ = link_to_markdown commit.title, project_commit_path(@project, commit.id), class: "cdark", title: commit.title
.pull-right
= link_to commit.short_id, project_commit_path(@project, commit), class: "commit-sha"
&nbsp;
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 32dbc1b3417..05b7dfe2872 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -19,7 +19,9 @@
= render 'shared/new_commit_form', placeholder: placeholder
.form-actions
- = button_tag button_title, class: 'btn btn-small btn-create btn-upload-file', id: 'submit-all'
+ = button_tag class: 'btn btn-create btn-upload-file', id: 'submit-all', type: 'button' do
+ = icon('spin spinner', class: 'js-loading-icon hidden' )
+ = button_title
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project)
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
deleted file mode 100644
index 2076e46fde8..00000000000
--- a/app/views/projects/boards/_show.html.haml
+++ /dev/null
@@ -1,41 +0,0 @@
-- @no_container = true
-- @content_class = "issue-boards-content"
-- page_title "Boards"
-
-- if show_new_nav?
- - add_to_breadcrumbs("Issues", project_issues_path(@project))
-
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
- = webpack_bundle_tag 'filtered_search'
- = webpack_bundle_tag 'boards'
-
- %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
- %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
-
-= render "projects/issues/head"
-
-.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-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- %board{ "v-cloak" => true,
- "v-for" => "list in state.lists",
- "ref" => "board",
- ":list" => "list",
- ":disabled" => "disabled",
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath",
- ":board-id" => "boardId",
- ":key" => "_uid" }
- = render "projects/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) }
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
deleted file mode 100644
index 64f5f6d7ba0..00000000000
--- a/app/views/projects/boards/components/_board.html.haml
+++ /dev/null
@@ -1,46 +0,0 @@
-.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded }',
- ":data-id" => "list.id" }
- .board-inner
- %header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" }
- %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' }
- %i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable",
- ":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded && list.position === -1, \"fa-caret-left\": !list.isExpanded && list.position !== -1 }",
- "aria-hidden": "true" }
-
- %span.has-tooltip{ "v-if": "list.type !== \"label\"",
- ":title" => '(list.label ? list.label.description : "")' }
- {{ list.title }}
-
- %span.has-tooltip{ "v-if": "list.type === \"label\"",
- ":title" => '(list.label ? list.label.description : "")',
- data: { container: "body", placement: "bottom" },
- class: "label color-label title",
- ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.color ? list.label.text_color : \"#2e2e2e\") }" }
- {{ list.title }}
- .issue-count-badge.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
- %span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
- {{ list.issuesSize }}
- - if can?(current_user, :admin_issue, @project)
- %button.issue-count-badge-add-button.btn.btn-small.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
- "@click" => "showNewIssueForm",
- "v-if" => 'list.type !== "closed"',
- "aria-label" => "New issue",
- "title" => "New issue",
- data: { placement: "top", container: "body" } }
- = icon("plus", class: "js-no-trigger-collapse")
- - if can?(current_user, :admin_list, @project)
- %board-delete{ "inline-template" => true,
- ":list" => "list",
- "v-if" => "!list.preset && list.id" }
- %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
- = icon("trash")
- %board-list{ "v-if" => 'list.type !== "blank"',
- ":list" => "list",
- ":issues" => "list.issues",
- ":loading" => "list.loading",
- ":disabled" => "disabled",
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath",
- "ref" => "board-list" }
- - if can?(current_user, :admin_list, @project)
- %board-blank-state{ "v-if" => 'list.id == "blank"' }
diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml
deleted file mode 100644
index 09d70f658a3..00000000000
--- a/app/views/projects/boards/components/_sidebar.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-%board-sidebar{ "inline-template" => true,
- ":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" }
- %transition{ name: "boards-sidebar-slide" }
- %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
- .issuable-sidebar
- .block.issuable-sidebar-header
- %span.issuable-header-text.hide-collapsed.pull-left
- %strong
- {{ issue.title }}
- %br/
- %span
- = precede "#" do
- {{ issue.id }}
- %a.gutter-toggle.pull-right{ role: "button",
- href: "#",
- "@click.prevent" => "closeSidebar",
- "aria-label" => "Toggle sidebar" }
- = custom_icon("icon_close", size: 15)
- .js-issuable-update
- = render "projects/boards/components/sidebar/assignee"
- = render "projects/boards/components/sidebar/milestone"
- = render "projects/boards/components/sidebar/due_date"
- = render "projects/boards/components/sidebar/labels"
- = render "projects/boards/components/sidebar/notifications"
- %remove-btn{ ":issue" => "issue",
- ":list" => "list",
- "v-if" => "canRemove" }
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
deleted file mode 100644
index 8d957613be1..00000000000
--- a/app/views/projects/boards/components/sidebar/_assignee.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-.block.assignee{ ref: "assigneeBlock" }
- %template{ "v-if" => "issue.assignees" }
- %assignee-title{ ":number-of-assignees" => "issue.assignees.length",
- ":loading" => "loadingAssignees",
- ":editable" => can?(current_user, :admin_issue, @project) }
- %assignees.value{ "root-path" => "#{root_url}",
- ":users" => "issue.assignees",
- ":editable" => can?(current_user, :admin_issue, @project),
- "@assign-self" => "assignSelf" }
-
- - if can?(current_user, :admin_issue, @project)
- .selectbox.hide-collapsed
- %input.js-vue{ type: "hidden",
- name: "issue[assignee_ids][]",
- ":value" => "assignee.id",
- "v-if" => "issue.assignees",
- "v-for" => "assignee in issue.assignees",
- ":data-avatar_url" => "assignee.avatar",
- ":data-name" => "assignee.name",
- ":data-username" => "assignee.username" }
- .dropdown
- - dropdown_options = issue_assignees_dropdown_options
- %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: { toggle: 'dropdown', field_name: 'issue[assignee_ids][]', first_user: current_user&.username, current_user: 'true', project_id: @project.id, null_user: 'true', multi_select: 'true', 'dropdown-header': dropdown_options[:data][:'dropdown-header'], 'max-select': dropdown_options[:data][:'max-select'] },
- ":data-issuable-id" => "issue.id",
- ":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
- = dropdown_options[:title]
- = icon("chevron-down")
- .dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
- = dropdown_title("Assign to")
- = dropdown_filter("Search users")
- = dropdown_content
- = dropdown_loading
diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml
deleted file mode 100644
index f44a9d49a54..00000000000
--- a/app/views/projects/boards/components/sidebar/_due_date.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-.block.due_date
- .title
- Due date
- - if can?(current_user, :admin_issue, @project)
- = icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "edit-link pull-right"
- .value
- .value-content
- %span.no-value{ "v-if" => "!issue.dueDate" }
- No due date
- %span.bold{ "v-if" => "issue.dueDate" }
- {{ issue.dueDate | due-date }}
- - if can?(current_user, :admin_issue, @project)
- %span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
- \-
- %a.js-remove-due-date{ href: "#", role: "button" }
- remove due date
- - if can?(current_user, :admin_issue, @project)
- .selectbox
- %input{ type: "hidden",
- name: "issue[due_date]",
- ":value" => "issue.dueDate" }
- .dropdown
- %button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
- data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" },
- ":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
- %span.dropdown-toggle-text Due date
- = icon('chevron-down')
- .dropdown-menu.dropdown-menu-due-date
- = dropdown_title('Due date')
- = dropdown_content do
- .js-due-date-calendar
diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml
deleted file mode 100644
index 7d0c35fe183..00000000000
--- a/app/views/projects/boards/components/sidebar/_labels.html.haml
+++ /dev/null
@@ -1,30 +0,0 @@
-.block.labels
- .title
- Labels
- - if can?(current_user, :admin_issue, @project)
- = icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "edit-link pull-right"
- .value.issuable-show-labels
- %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
- None
- %a{ href: "#",
- "v-for" => "label in issue.labels" }
- %span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
- {{ label.title }}
- - if can?(current_user, :admin_issue, @project)
- .selectbox
- %input{ type: "hidden",
- name: "issue[label_names][]",
- "v-for" => "label in issue.labels",
- ":value" => "label.id" }
- .dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
- data: { toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", project_id: @project.id, labels: project_labels_path(@project, :json), namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path) },
- ":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
- %span.dropdown-toggle-text
- Label
- = icon('chevron-down')
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
- = render partial: "shared/issuable/label_page_default"
- - if can? current_user, :admin_label, @project and @project
- = render partial: "shared/issuable/label_page_create"
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
deleted file mode 100644
index 002e9994ee0..00000000000
--- a/app/views/projects/boards/components/sidebar/_milestone.html.haml
+++ /dev/null
@@ -1,29 +0,0 @@
-.block.milestone
- .title
- Milestone
- - if can?(current_user, :admin_issue, @project)
- = icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "edit-link pull-right"
- .value
- %span.no-value{ "v-if" => "!issue.milestone" }
- None
- %span.bold.has-tooltip{ "v-if" => "issue.milestone" }
- {{ issue.milestone.title }}
- - if can?(current_user, :admin_issue, @project)
- .selectbox
- %input{ type: "hidden",
- ":value" => "issue.milestone.id",
- name: "issue[milestone_id]",
- "v-if" => "issue.milestone" }
- .dropdown
- %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: project_milestones_path(@project, :json), ability_name: "issue", use_id: "true", default_no: "true" },
- ":data-selected" => "milestoneTitle",
- ":data-issuable-id" => "issue.id",
- ":data-issue-update" => "'#{project_issues_path(@project)}/' + issue.id + '.json'" }
- Milestone
- = icon("chevron-down")
- .dropdown-menu.dropdown-select.dropdown-menu-selectable
- = dropdown_title("Assign milestone")
- = dropdown_filter("Search milestones")
- = dropdown_content
- = dropdown_loading
diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/projects/boards/components/sidebar/_notifications.html.haml
deleted file mode 100644
index aaddd7e249f..00000000000
--- a/app/views/projects/boards/components/sidebar/_notifications.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-- if current_user
- .block.light.subscription{ ":data-url" => "'#{project_issues_path(@project)}/' + issue.id + '/toggle_subscription'" }
- %span.issuable-header-text.hide-collapsed.pull-left
- Notifications
- %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
- %span
- {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml
index 2a5b8b1441e..bb56769bd3f 100644
--- a/app/views/projects/boards/index.html.haml
+++ b/app/views/projects/boards/index.html.haml
@@ -1 +1 @@
-= render "show"
+= render "shared/boards/show", board: @boards.first
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
index 2a5b8b1441e..e5b5f6404bb 100644
--- a/app/views/projects/boards/show.html.haml
+++ b/app/views/projects/boards/show.html.haml
@@ -1 +1 @@
-= render "show"
+= render "shared/boards/show", board: @board
diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml
index 18fbb81c167..7892019bb15 100644
--- a/app/views/projects/branches/_commit.html.haml
+++ b/app/views/projects/branches/_commit.html.haml
@@ -4,6 +4,6 @@
= link_to commit.short_id, project_commit_path(project, commit.id), class: "commit-sha"
&middot;
%span.str-truncated
- = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message"
+ = link_to_markdown commit.title, project_commit_path(project, commit.id), class: "commit-row-message"
&middot;
#{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 945a5c11d6d..73583c6bbc2 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -2,9 +2,6 @@
- page_title "Branches"
= render "projects/commits/head"
-- if show_new_nav?
- - add_to_breadcrumbs("Repository", project_tree_path(@project))
-
%div{ class: container_class }
.top-area.adjust
- if can?(current_user, :admin_project, @project)
diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml
index 22674b671c9..83821326aec 100644
--- a/app/views/projects/commit/_ajax_signature.html.haml
+++ b/app/views/projects/commit/_ajax_signature.html.haml
@@ -1,3 +1,2 @@
- if commit.has_signature?
%button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } }
- %i.fa.fa-spinner.fa-spin
diff --git a/app/views/projects/commit/_invalid_signature_badge.html.haml b/app/views/projects/commit/_invalid_signature_badge.html.haml
deleted file mode 100644
index 3a73aae9d95..00000000000
--- a/app/views/projects/commit/_invalid_signature_badge.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-- title = capture do
- .gpg-popover-icon.invalid
- = render 'shared/icons/icon_status_notfound_borderless.svg'
- %div
- This commit was signed with an <strong>unverified</strong> signature.
-
-- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] }
-
-= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_other_user_signature_badge.html.haml b/app/views/projects/commit/_other_user_signature_badge.html.haml
new file mode 100644
index 00000000000..80eca96f7ce
--- /dev/null
+++ b/app/views/projects/commit/_other_user_signature_badge.html.haml
@@ -0,0 +1,6 @@
+- title = capture do
+ This commit was signed with a different user's verified signature.
+
+- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless', show_user: true }
+
+= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 09e3a775d1c..bef96786b73 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -2,6 +2,7 @@
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint,
"help-page-path" => help_page_path('ci/quick_start/README'),
+ "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
} }
- content_for :page_specific_javascripts do
diff --git a/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml
new file mode 100644
index 00000000000..e737de48e22
--- /dev/null
+++ b/app/views/projects/commit/_same_user_different_email_signature_badge.html.haml
@@ -0,0 +1,7 @@
+- title = capture do
+ This commit was signed with a verified signature, but the committer email
+ is <strong>not verified</strong> to belong to the same user.
+
+- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'icon_status_notfound_borderless', show_user: true }
+
+= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml
index 60fa52557ef..145bc629380 100644
--- a/app/views/projects/commit/_signature.html.haml
+++ b/app/views/projects/commit/_signature.html.haml
@@ -1,5 +1,2 @@
- if signature
- - if signature.valid_signature?
- = render partial: 'projects/commit/valid_signature_badge', locals: { signature: signature }
- - else
- = render partial: 'projects/commit/invalid_signature_badge', locals: { signature: signature }
+ = render partial: "projects/commit/#{signature.verification_status}_signature_badge", locals: { signature: signature }
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index 66f00eb5507..edff018ba6d 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -1,18 +1,28 @@
-- css_classes = commit_signature_badge_classes(css_classes)
+- signature = local_assigns.fetch(:signature)
+- title = local_assigns.fetch(:title)
+- label = local_assigns.fetch(:label)
+- css_class = local_assigns.fetch(:css_class)
+- icon = local_assigns.fetch(:icon)
+- show_user = local_assigns.fetch(:show_user, false)
+
+- css_classes = commit_signature_badge_classes(css_class)
- title = capture do
.gpg-popover-status
- = title
+ .gpg-popover-icon{ class: css_class }
+ = render "shared/icons/#{icon}.svg"
+ %div
+ = title
- content = capture do
- .clearfix
- = content
+ - if show_user
+ .clearfix
+ = render partial: 'projects/commit/signature_badge_user', locals: { signature: signature }
GPG Key ID:
%span.monospace= signature.gpg_key_primary_keyid
-
- = link_to('Learn more about signing commits', help_page_path('workflow/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
+ = link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
= label
diff --git a/app/views/projects/commit/_signature_badge_user.html.haml b/app/views/projects/commit/_signature_badge_user.html.haml
new file mode 100644
index 00000000000..b20198e76db
--- /dev/null
+++ b/app/views/projects/commit/_signature_badge_user.html.haml
@@ -0,0 +1,21 @@
+- gpg_key = signature.gpg_key
+- user = gpg_key&.user
+- user_name = signature.gpg_key_user_name
+- user_email = signature.gpg_key_user_email
+
+- if user
+ = link_to user_path(user), class: 'gpg-popover-user-link' do
+ %div
+ = user_avatar_without_link(user: user, size: 32)
+
+ %div
+ %strong= user.name
+ %div= user.to_reference
+- else
+ = mail_to user_email do
+ %div
+ = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
+
+ %div
+ %strong= user_name
+ %div= user_email
diff --git a/app/views/projects/commit/_unknown_key_signature_badge.html.haml b/app/views/projects/commit/_unknown_key_signature_badge.html.haml
new file mode 100644
index 00000000000..75c5cf57bcc
--- /dev/null
+++ b/app/views/projects/commit/_unknown_key_signature_badge.html.haml
@@ -0,0 +1 @@
+= render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature }
diff --git a/app/views/projects/commit/_unverified_key_signature_badge.html.haml b/app/views/projects/commit/_unverified_key_signature_badge.html.haml
new file mode 100644
index 00000000000..75c5cf57bcc
--- /dev/null
+++ b/app/views/projects/commit/_unverified_key_signature_badge.html.haml
@@ -0,0 +1 @@
+= render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature }
diff --git a/app/views/projects/commit/_unverified_signature_badge.html.haml b/app/views/projects/commit/_unverified_signature_badge.html.haml
new file mode 100644
index 00000000000..1af58027b83
--- /dev/null
+++ b/app/views/projects/commit/_unverified_signature_badge.html.haml
@@ -0,0 +1,6 @@
+- title = capture do
+ This commit was signed with an <strong>unverified</strong> signature.
+
+- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless' }
+
+= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_valid_signature_badge.html.haml b/app/views/projects/commit/_valid_signature_badge.html.haml
deleted file mode 100644
index db1a41bbf64..00000000000
--- a/app/views/projects/commit/_valid_signature_badge.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-- title = capture do
- .gpg-popover-icon.valid
- = render 'shared/icons/icon_status_success_borderless.svg'
- %div
- This commit was signed with a <strong>verified</strong> signature.
-
-- content = capture do
- - gpg_key = signature.gpg_key
- - user = gpg_key&.user
- - user_name = signature.gpg_key_user_name
- - user_email = signature.gpg_key_user_email
-
- - if user
- = link_to user_path(user), class: 'gpg-popover-user-link' do
- %div
- = user_avatar_without_link(user: user, size: 32)
-
- %div
- %strong= gpg_key.user.name
- %div @#{gpg_key.user.username}
- - else
- = mail_to user_email do
- %div
- = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
-
- %div
- %strong= user_name
- %div= user_email
-
-- locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] }
-
-= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/_verified_signature_badge.html.haml b/app/views/projects/commit/_verified_signature_badge.html.haml
new file mode 100644
index 00000000000..423beba2120
--- /dev/null
+++ b/app/views/projects/commit/_verified_signature_badge.html.haml
@@ -0,0 +1,7 @@
+- title = capture do
+ This commit was signed with a <strong>verified</strong> signature and the
+ committer email is verified to belong to the same user.
+
+- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'icon_status_success_borderless', show_user: true }
+
+= render partial: 'projects/commit/signature_badge', locals: locals
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 07c83c0a590..717de85c5d2 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- add_to_breadcrumbs "Commits", project_commits_path(@project)
+- breadcrumb_title @commit.short_id
- container_class = !fluid_layout && diff_view == :inline ? 'container-limited' : ''
- limited_container_width = fluid_layout ? '' : 'limit-container-width'
- @content_class = limited_container_width
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index e7da47032be..b8655808d89 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -5,7 +5,7 @@
- notes = commit.notes
- note_count = notes.user.count
-- cache_key = [project.full_path, commit.id, current_application_settings, note_count, @path.presence, current_controller?(:commits)]
+- cache_key = [project.full_path, commit.id, current_application_settings, note_count, @path.presence, current_controller?(:commits), I18n.locale]
- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
@@ -16,7 +16,7 @@
.commit-detail
.commit-content
- = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message item-title"
+ = link_to_markdown_field(commit, :title, project_commit_path(project, commit.id), class: "commit-row-message item-title")
%span.commit-row-message.visible-xs-inline
&middot;
= commit.short_id
@@ -28,7 +28,8 @@
- if commit.description?
%pre.commit-row-description.js-toggle-content
- = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
+ = preserve(markdown_field(commit, :description))
+
.commiter
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
- commit_timeago = time_ago_with_tooltip(commit.committed_date)
@@ -37,14 +38,14 @@
.commit-actions.hidden-xs
- - if commit.status(ref)
- = render_commit_status(commit, ref: ref)
-
- if request.xhr?
= render partial: 'projects/commit/signature', object: commit.signature
- else
= render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
+ - if commit.status(ref)
+ = render_commit_status(commit, ref: ref)
+
= link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha btn btn-transparent"
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
= link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/_inline_commit.html.haml b/app/views/projects/commits/_inline_commit.html.haml
index 48cefbe45f2..26385d2f534 100644
--- a/app/views/projects/commits/_inline_commit.html.haml
+++ b/app/views/projects/commits/_inline_commit.html.haml
@@ -3,6 +3,6 @@
= link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha"
&nbsp;
%span.str-truncated
- = link_to_gfm commit.title, project_commit_path(project, commit.id), class: "commit-row-message"
+ = link_to_markdown_field(commit, :title, project_commit_path(project, commit.id), class: "commit-row-message")
.pull-right
#{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 7ae56086177..e873b931683 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")
-- if show_new_nav?
- - add_to_breadcrumbs("Repository", project_tree_path(@project))
-
= content_for :sub_nav do
= render "head"
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index 05de21e8dbf..2632fea6eba 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
+- breadcrumb_title "Compare Revisions"
- page_title "Compare"
-- if show_new_nav?
- - add_to_breadcrumbs("Repository", project_tree_path(@project))
= render "projects/commits/head"
%div{ class: container_class }
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 8bc863f77b3..7cc42455394 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -1,8 +1,6 @@
- @no_container = true
-- breadcrumb_title "Compare"
+- add_to_breadcrumbs "Compare Revisions", project_compare_index_path(@project)
- page_title "#{params[:from]}...#{params[:to]}"
-- if show_new_nav?
- - add_to_breadcrumbs("Repository", project_tree_path(@project))
= render "projects/commits/head"
%div{ class: container_class }
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index c704635ead3..8d008be5aae 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,7 +1,5 @@
- @no_container = true
- page_title "Cycle Analytics"
-- if show_new_nav?
- - add_to_breadcrumbs("Project", project_path(@project))
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('cycle_analytics')
@@ -40,6 +38,9 @@
%i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right
%li
+ %a{ "href" => "#", "data-value" => "7" }
+ {{ n__('Last %d day', 'Last %d days', 7) }}
+ %li
%a{ "href" => "#", "data-value" => "30" }
{{ n__('Last %d day', 'Last %d days', 30) }}
%li
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index 4c22166c256..014486be868 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -12,6 +12,6 @@
%span.flex-truncate-child
- if commit_title = deployment.commit_title
= author_avatar(deployment.commit, size: 20)
- = link_to_gfm commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message"
+ = link_to_markdown commit_title, project_commit_path(@project, deployment.sha), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 8c8aa4c78f5..376f672f424 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -2,22 +2,24 @@
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
+- merge_request = local_assigns.fetch(:merge_request, false)
-.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed
- .inline-parallel-buttons
- - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
- = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
- - if show_whitespace_toggle
- - if current_controller?(:commit)
- = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs')
- - elsif current_controller?('projects/merge_requests/diffs')
- = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs')
- - elsif current_controller?(:compare)
- = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs')
- .btn-group
- = inline_diff_btn
- = parallel_diff_btn
- = render 'projects/diffs/stats', diff_files: diff_files
+.content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed{ class: ("diff-files-changed-merge-request" if merge_request) }
+ .files-changed-inner
+ .inline-parallel-buttons.hidden-xs.hidden-sm
+ - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
+ = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
+ - if show_whitespace_toggle
+ - if current_controller?(:commit)
+ = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs')
+ - elsif current_controller?('projects/merge_requests/diffs')
+ = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs')
+ - elsif current_controller?(:compare)
+ = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs')
+ .btn-group
+ = inline_diff_btn
+ = parallel_diff_btn
+ = render 'projects/diffs/stats', diff_files: diff_files
- if render_overflow_warning?(diff_files)
= render 'projects/diffs/warning', diff_files: diffs
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index efc0ea31917..ad2d355ab4a 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -10,7 +10,7 @@
%strong.cgreen #{sum_added_lines} additions
and
%strong.cred #{sum_removed_lines} deletions
- .diff-stats-additions-deletions-collapsed.pull-right{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
+ .diff-stats-additions-deletions-collapsed.pull-right.hidden-xs.hidden-sm{ "aria-hidden": "true", "aria-describedby": "diff-stats" }
%strong.cgreen<
+#{sum_added_lines}
%strong.cred<
@@ -29,6 +29,6 @@
+#{diff_file.added_lines}
%span.cred<
\-#{diff_file.removed_lines}
- %li.dropdown-menu-empty-link.hidden
- %a{ href: "#" }
+ %li.dropdown-menu-empty-item.hidden
+ %a
No files found.
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 20fceda26dc..0a3045604f4 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,3 +1,4 @@
+- breadcrumb_title "General Settings"
- page_title "General"
- @content_class = "limit-container-width" unless fluid_layout
- expanded = Rails.env.test?
@@ -65,90 +66,18 @@
%section.settings.sharing-permissions
.settings-header
%h4
- Sharing and permissions
+ Permissions
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Enable or disable certain project features and choose access levels.
.settings-content.no-animate{ class: ('expanded' if expanded) }
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
- .form_group.sharing-and-permissions
- .row.js-visibility-select
- .col-md-8
- .label-light
- = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
- = link_to icon('question-circle'), help_page_path("public_access/public_access")
- %span.help-block
- .col-md-4.visibility-select-container
- = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
- = f.fields_for :project_feature do |feature_fields|
- %fieldset.features
- .row
- .col-md-8.project-feature
- = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
- %span.help-block View and edit files in this project
- .col-md-4.js-repo-access-level
- = project_feature_access_select(:repository_access_level)
-
- .row
- .col-md-8.project-feature.nested
- = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
- %span.help-block Submit changes to be merged upstream
- .col-md-4
- = project_feature_access_select(:merge_requests_access_level)
-
- .row
- .col-md-8.project-feature.nested
- = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light'
- %span.help-block Build, test, and deploy your changes
- .col-md-4
- = project_feature_access_select(:builds_access_level)
-
- .row
- .col-md-8.project-feature
- = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
- %span.help-block Share code pastes with others out of Git repository
- .col-md-4
- = project_feature_access_select(:snippets_access_level)
-
- .row
- .col-md-8.project-feature
- = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
- %span.help-block Lightweight issue tracking system for this project
- .col-md-4
- = project_feature_access_select(:issues_access_level)
-
- .row
- .col-md-8.project-feature
- = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
- %span.help-block Pages for project documentation
- .col-md-4
- = project_feature_access_select(:wiki_access_level)
- .form-group
- = render 'shared/allow_request_access', form: f
- - if Gitlab.config.lfs.enabled && current_user.admin?
- .row.js-lfs-enabled.form-group.sharing-and-permissions
- .col-md-8
- = f.label :lfs_enabled, 'Git Large File Storage', class: 'label-light'
- = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- %span.help-block Manages large files such as audio, video and graphics files.
- .col-md-4
- .select-wrapper
- = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' }
- = icon('chevron-down')
- - if Gitlab.config.registry.enabled
- .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) }
- .checkbox
- = f.label :container_registry_enabled do
- = f.check_box :container_registry_enabled
- %strong Container Registry
- %br
- %span.descr Enable Container Registry for this project
- = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
+ %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
+ .js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save"
-
- %section.settings.merge-requests-feature{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) }
+ %section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) }
.settings-header
%h4
Merge request settings
@@ -161,42 +90,7 @@
= render 'merge_request_settings', form: f
= f.submit 'Save changes', class: "btn btn-save"
- %section.settings
- .settings-header
- %h4
- Export project
- %button.btn.js-settings-toggle
- = expanded ? 'Collapse' : 'Expand'
- %p
- Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
- .settings-content.no-animate{ class: ('expanded' if expanded) }
- .bs-callout.bs-callout-info
- %p.append-bottom-0
- %p
- The following items will be exported:
- %ul
- %li Project and wiki repositories
- %li Project uploads
- %li Project configuration including web hooks and services
- %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
- %p
- The following items will NOT be exported:
- %ul
- %li Job traces and artifacts
- %li LFS objects
- %li Container registry images
- %li CI variables
- %li Any encrypted tokens
- %p
- Once the exported file is ready, you will receive a notification email with a download link.
- - if @project.export_project_path
- = link_to 'Download export', download_export_project_path(@project),
- rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
- = link_to 'Generate new export', generate_new_export_project_path(@project),
- method: :post, class: "btn btn-default"
- - else
- = link_to 'Export project', export_project_path(@project),
- method: :post, class: "btn btn-default"
+ = render 'export', project: @project
%section.settings.advanced-settings
.settings-header
@@ -205,7 +99,7 @@
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
- Perform advanced options such as housekeeping, exporting, archiveing, renameing, transfering, or removeing your project.
+ Perform advanced options such as housekeeping, exporting, archiving, renaming, transferring, or removing your project.
.settings-content.no-animate{ class: ('expanded' if expanded) }
.sub-section
%h4 Housekeeping
@@ -237,8 +131,6 @@
.sub-section.rename-respository
%h4.warning-title
Rename repository
- %p
- Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
= render 'projects/errors'
= form_for([@project.namespace.becomes(Namespace), @project]) do |f|
.form-group.project_name_holder
@@ -274,7 +166,7 @@
%li Be careful. Changing the project's namespace can have unintended side effects.
%li You can only transfer the project to namespaces you manage.
%li You will need to update your local repositories to point to the new location.
- %li Project visibility level will be changed to match namespace rules when transfering to a group.
+ %li Project visibility level will be changed to match namespace rules when transferring to a group.
= f.submit 'Transfer project', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
- if @project.forked? && can?(current_user, :remove_fork_project, @project)
.sub-section
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index d17709380d5..5e980314307 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,4 +1,5 @@
- @no_container = true
+- breadcrumb_title "Details"
= render partial: 'flash_messages', locals: { project: @project }
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index d0f723af5bf..acc80b49dd0 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -1,10 +1,8 @@
- @no_container = true
- page_title "Environments"
+- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
= render "projects/pipelines/head"
-- if show_new_nav?
- - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
-
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag("environments")
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 0ce0f5465fc..c35d1b5aaee 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- add_to_breadcrumbs "Environments", project_environments_path(@project)
+- breadcrumb_title @environment.name
- page_title "Environments"
= render "projects/pipelines/head"
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index 9f5a1239a82..f0ef647ddb3 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -1,7 +1,5 @@
- @no_container = true
- page_title "Charts"
-- if show_new_nav?
- - add_to_breadcrumbs("Repository", project_tree_path(@project))
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_d3')
= webpack_bundle_tag('graphs')
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index f41a0d8293b..08b38428b50 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -5,9 +5,6 @@
= webpack_bundle_tag('graphs')
= webpack_bundle_tag('graphs_show')
-- if show_new_nav?
- - add_to_breadcrumbs("Repository", project_tree_path(@project))
-
= render 'projects/commits/head'
.js-graphs-show{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) }
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 8b095f4ca10..483f28c74f2 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,7 +1,17 @@
+- @gfm_form = true
+
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
-#notes
- = render 'shared/notes/notes_with_form', :autocomplete => true
+%section.js-vue-notes-event
+ #js-vue-notes{ data: { discussions_path: discussions_project_issue_path(@project, @issue, format: :json),
+ register_path: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
+ new_session_path: new_session_path(:user, redirect_to_referer: 'yes'),
+ markdown_docs_path: help_page_path('user/markdown'),
+ quick_actions_docs_path: help_page_path('user/project/quick_actions'),
+ notes_path: notes_url,
+ last_fetched_at: Time.now.to_i,
+ issue_data: serialize_issuable(@issue),
+ current_user_data: UserSerializer.new.represent(current_user).to_json } }
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index 34d5a3e1831..6fb5aa45166 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -4,4 +4,4 @@
= render 'shared/empty_states/issues'
- if @issues.present?
- = paginate @issues, theme: "gitlab"
+ = paginate @issues, theme: "gitlab", total_pages: @total_pages
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index 756faf4625e..13809da6523 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -1,7 +1,7 @@
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- if @can_bulk_update
- = button_tag "Edit Issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
+ = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle"
= link_to "New issue", new_project_issue_path(@project,
issue: { assignee_id: issues_finder.assignee.try(:id),
milestone_id: issues_finder.milestones.first.try(:id) }),
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index aacb057840d..e72c94695bc 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -13,15 +13,11 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@project.name} issues")
-- if show_new_nav?
- - content_for :breadcrumbs_extra do
- = render "projects/issues/nav_btns"
-
- if project_issues(@project).exists?
%div{ class: (container_class) }
.top-area
= render 'shared/issuable/nav', type: :issues
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls
= render "projects/issues/nav_btns"
= render 'shared/issuable/search_bar', type: :issues
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index ad5befc6ee5..fbaf88356bf 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,7 +1,14 @@
- @content_class = "limit-container-width" unless fluid_layout
+- add_to_breadcrumbs "Issues", project_issues_path(@project)
+- breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
+
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'notes'
+
- can_update_issue = can?(current_user, :update_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
@@ -23,7 +30,7 @@
= icon('eye-slash', class: 'is-confidential')
= issuable_meta(@issue, @project, "Issue")
- .issuable-actions
+ .issuable-actions.js-issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
Options
@@ -35,8 +42,8 @@
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
- %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- %li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ %li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- if can_report_spam
%li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
- if can_update_issue || can_report_spam
@@ -73,7 +80,7 @@
.content-block.emoji-block
.row
- .col-sm-8
+ .col-sm-8.js-issue-note-awards
= render 'award_emoji/awards_block', awardable: @issue, inline: true
.col-sm-4.new-branch-col
= render 'new_branch' unless @issue.confidential?
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 7aee9212a45..d820fc40ad7 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -46,14 +46,14 @@
%span.build-light-text Token:
#{@build.trigger_request.trigger.short_token}
- - if @build.trigger_request.variables
+ - if @build.trigger_variables.any?
%p
%button.btn.group.btn-group-justified.reveal-variables Reveal Variables
%dl.js-build-variables.trigger-build-variables.hide
- - @build.trigger_request.variables.each do |key, value|
- %dt.js-build-variable.trigger-build-variable= key
- %dd.js-build-value.trigger-build-value= value
+ - @build.trigger_variables.each do |trigger_variable|
+ %dt.js-build-variable.trigger-build-variable= trigger_variable[:key]
+ %dd.js-build-value.trigger-build-value= trigger_variable[:value]
%div{ class: (@build.pipeline.stages_count > 1 ? "block" : "block-last") }
%p
@@ -75,7 +75,7 @@
Pipeline
= link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit'
from
- = link_to "#{@build.pipeline.ref}", project_branch_path(@project, @build.pipeline.ref), class: 'link-commit'
+ = link_to "#{@build.pipeline.ref}", project_ref_path(@project, @build.pipeline.ref), class: 'link-commit ref-name'
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.stage-selection More
= icon('chevron-down')
diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml
index d78891546f7..8604c7d3ea4 100644
--- a/app/views/projects/jobs/index.html.haml
+++ b/app/views/projects/jobs/index.html.haml
@@ -2,9 +2,6 @@
- page_title "Jobs"
= render "projects/pipelines/head"
-- if show_new_nav?
- - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
-
%div{ class: container_class }
.top-area
- build_path_proc = ->(scope) { project_jobs_path(@project, scope: scope) }
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index fa086413fbe..975c08c06e6 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- add_to_breadcrumbs "Jobs", project_jobs_path(@project)
+- breadcrumb_title "##{@build.id}"
- page_title "#{@build.name} (##{@build.id})", "Jobs"
= render "projects/pipelines/head"
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 4b9da02c6b8..10d07ce8e45 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -3,10 +3,6 @@
- hide_class = ''
- can_admin_label = can?(current_user, :admin_label, @project)
-- if show_new_nav? && can?(current_user, :admin_label, @project)
- - content_for :breadcrumbs_extra do
- = link_to "New label", new_namespace_project_label_path(@project.namespace, @project), class: "btn btn-new"
-
= render "shared/mr_head"
- if @labels.exists? || @prioritized_labels.exists?
@@ -18,7 +14,7 @@
Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.
- if can_admin_label
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls
= link_to new_project_label_path(@project), class: "btn btn-new" do
New label
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index b787edb3427..3303aa72604 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -4,8 +4,8 @@
= link_to 'Close merge request', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: { original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- if @merge_request.reopenable?
= link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-close js-note-target-reopen", title: "Reopen merge request", data: { original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
- %comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" }
- %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
- {{ buttonText }}
+ %comment-and-resolve-btn{ "inline-template" => true }
+ %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
+ {{ buttonText }}
#notes= render "shared/notes/notes_with_form", :autocomplete => true
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index 4e97f74dd6a..bd6f1c05949 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -5,4 +5,4 @@
= render 'shared/empty_states/merge_requests'
- if @merge_requests.present?
- = paginate @merge_requests, theme: "gitlab"
+ = paginate @merge_requests, theme: "gitlab", total_pages: @total_pages
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index a2e819fb3a7..f3c44c94a5c 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -17,7 +17,7 @@
.issuable-meta
= issuable_meta(@merge_request, @project, "Merge request")
- .issuable-actions
+ .issuable-actions.js-issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
Options
diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml
index e92f2712347..e73dab8ad4a 100644
--- a/app/views/projects/merge_requests/_nav_btns.html.haml
+++ b/app/views/projects/merge_requests/_nav_btns.html.haml
@@ -1,5 +1,5 @@
- if @can_bulk_update
- = button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle"
+ = button_tag "Edit merge requests", class: "btn append-right-10 js-bulk-update-toggle"
- if merge_project
= link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do
New merge request
diff --git a/app/views/projects/merge_requests/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml
index fb31e2fef00..0d30d6da68f 100644
--- a/app/views/projects/merge_requests/diffs/_diffs.html.haml
+++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml
@@ -1,5 +1,5 @@
- if @merge_request_diff.collected? || @merge_request_diff.overflow?
= render 'projects/merge_requests/diffs/versions'
- = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index c020e7db380..2c53891a92d 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -12,17 +12,15 @@
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'filtered_search'
-- if show_new_nav?
- - content_for :breadcrumbs_extra do
- = render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
-
= render 'projects/last_push'
- if @project.merge_requests.exists?
%div{ class: container_class }
+ - if show_auto_devops_callout?(@project)
+ = render 'shared/auto_devops_callout'
.top-area
= render 'shared/issuable/nav', type: :merge_requests
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls
= render "projects/merge_requests/nav_btns", merge_project: merge_project, new_merge_request_path: new_merge_request_path
= render 'shared/issuable/search_bar', type: :merge_requests
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index d27e121beb4..c2d16f7e731 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -1,4 +1,6 @@
- @content_class = "limit-container-width" unless fluid_layout
+- add_to_breadcrumbs "Merge Requests", project_merge_requests_path(@project)
+- breadcrumb_title @merge_request.to_reference
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index e0b29b0c2e1..f3abecdd302 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,20 +1,16 @@
- @no_container = true
- page_title 'Milestones'
-- if show_new_nav? && can?(current_user, :admin_milestone, @project)
- - content_for :breadcrumbs_extra do
- = link_to "New milestone", new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone'
-
= render "shared/mr_head"
%div{ class: container_class }
.top-area
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
- .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) }
+ .nav-controls
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project)
- = link_to new_project_milestone_path(@project), class: "btn btn-new #{("visible-xs" if show_new_nav?)}", title: 'New milestone' do
+ = link_to new_project_milestone_path(@project), class: "btn btn-new", title: 'New milestone' do
New milestone
.milestones
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 0bf0e11c107..1f5f18801ad 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- add_to_breadcrumbs "Milestones", project_milestones_path(@project)
+- breadcrumb_title @milestone.title
- page_title @milestone.title, "Milestones"
- page_description @milestone.description
= render "shared/mr_head"
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index ab948df4a3f..e29cb277389 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -2,8 +2,6 @@
- page_title "Graph", @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('network')
-- if show_new_nav?
- - add_to_breadcrumbs("Repository", project_tree_path(@project))
= render "projects/commits/head"
= render "head"
%div{ class: container_class }
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index e3bbebbcf4c..819392b8f0c 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -25,11 +25,11 @@
.form-group
= f.label :template_project, class: 'label-light' do
Create from template
- = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'}
+ = link_to icon('question-circle'), help_page_path("gitlab-basics/create-project"), target: '_blank', aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'}
%div
= render 'project_templates', f: f
- .second-column
- - if import_sources_enabled?
+ - if import_sources_enabled?
+ .second-column
.project-import
.form-group.clearfix
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong
@@ -68,9 +68,10 @@
- if git_import_enabled?
%button.btn.js-toggle-button.import_git{ type: "button" }
= icon('git', text: 'Repo by URL')
- .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
- = icon('gitlab', text: 'GitLab export')
+ - if gitlab_project_import_enabled?
+ .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
+ = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
+ = icon('gitlab', text: 'GitLab export')
.row
.col-lg-12
@@ -111,7 +112,7 @@
%span.light (optional)
= f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250
- .form-group.project-visibility-level-holder
+ .form-group.visibility-level-setting
= f.label :visibility_level, class: 'label-light' do
Visibility Level
= link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
index 9c42be4e0ff..de76832331a 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -1,6 +1,8 @@
-- access = note_max_access_for_user(note)
-- if access
- %span.note-role= access
+- 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.") }
+ = issuable_first_contribution_icon
+- if access = note_max_access_for_user(note)
+ %span.note-role.note-role-access= Gitlab::Access.human_access(access)
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
@@ -17,24 +19,36 @@
"inline-template" => true,
"ref" => "note_#{note.id}" }
- %button.note-action-button.line-resolve-btn{ type: "button",
- class: ("is-disabled" unless can_resolve),
- ":class" => "{ 'is-active': isResolved }",
- ":aria-label" => "buttonText",
- "@click" => "resolve",
- ":title" => "buttonText",
- ":ref" => "'button'" }
+ .note-actions-item
+ %button.note-action-button.line-resolve-btn{ type: "button",
+ class: ("is-disabled" unless can_resolve),
+ ":class" => "{ 'is-active': isResolved }",
+ ":aria-label" => "buttonText",
+ "@click" => "resolve",
+ ":title" => "buttonText",
+ ":ref" => "'button'" }
- = icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
- %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
+ = icon('spin spinner', 'v-if' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
+ %div{ 'v-else' => '' }
+ %template{ 'v-if' => 'isResolved' }
+ = render 'shared/icons/icon_status_success_solid.svg'
+ %template{ 'v-else' => '' }
+ = render 'shared/icons/icon_resolve_discussion.svg'
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
- = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do
- = icon('spinner spin')
- %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
- %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
- %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
+ .note-actions-item
+ = button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip btn btn-transparent", data: { position: 'right', container: 'body' } do
+ = icon('spinner spin')
+ %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
+ %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
+ %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
+ - if note_editable
+ .note-actions-item
+ = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do
+ %span.link-highlight
+ = custom_icon('icon_pencil')
+
+ = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml
index 75a4687e1e3..88085c7185b 100644
--- a/app/views/projects/notes/_more_actions_dropdown.html.haml
+++ b/app/views/projects/notes/_more_actions_dropdown.html.haml
@@ -1,14 +1,13 @@
- is_current_user = current_user == note.author
- if note_editable || !is_current_user
- .dropdown.more-actions
+ .dropdown.more-actions.note-actions-item
= button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do
- = icon('ellipsis-v', class: 'icon')
+ %span.icon
+ = custom_icon('ellipsis_v')
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
- - if note_editable
- %li
- = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent'
- %li.divider
+ %li
+ = clipboard_button(text: noteable_note_url(note), title: 'Copy reference to clipboard', button_text: 'Copy link', class: 'btn-clipboard', hide_tooltip: true, hide_button_icon: true)
- unless is_current_user
%li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
diff --git a/app/views/projects/pipeline_schedules/edit.html.haml b/app/views/projects/pipeline_schedules/edit.html.haml
index 9b2a7b5821d..d95fa6da903 100644
--- a/app/views/projects/pipeline_schedules/edit.html.haml
+++ b/app/views/projects/pipeline_schedules/edit.html.haml
@@ -1,3 +1,5 @@
+- add_to_breadcrumbs _("Schedules"), pipeline_schedules_path(@project)
+- breadcrumb_title "##{@schedule.id}"
- page_title _("Edit"), @schedule.description, _("Pipeline Schedule")
%h3.page-title
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index 8426b29bb14..2b081786b6a 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -1,4 +1,4 @@
-- breadcrumb_title "Schedules"
+- breadcrumb_title _("Schedules")
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
@@ -7,12 +7,6 @@
- @no_container = true
- page_title _("Pipeline Schedules")
-- if show_new_nav? && can?(current_user, :create_pipeline_schedule, @project)
- - content_for :breadcrumbs_extra do
- = link_to _('New schedule'), new_namespace_project_pipeline_schedule_path(@project.namespace, @project), class: 'btn btn-create'
-
- - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
-
= render "projects/pipelines/head"
%div{ class: container_class }
@@ -22,7 +16,7 @@
= render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope
- if can?(current_user, :create_pipeline_schedule, @project)
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls
= link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-create' do
%span= _('New schedule')
diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml
index c7237cb96d8..cfdaf6d43bb 100644
--- a/app/views/projects/pipeline_schedules/new.html.haml
+++ b/app/views/projects/pipeline_schedules/new.html.haml
@@ -2,8 +2,7 @@
- @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project)
- page_title _("New Pipeline Schedule")
-- if show_new_nav?
- - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
+- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
%h3.page-title
= _("Schedule a new pipeline")
diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml
index fd3ad69d85d..487ac87186d 100644
--- a/app/views/projects/pipelines/charts.html.haml
+++ b/app/views/projects/pipelines/charts.html.haml
@@ -1,7 +1,6 @@
- @no_container = true
+- breadcrumb_title "CI / CD Charts"
- page_title _("Charts"), _("Pipelines")
-- if show_new_nav?
- - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('graphs')
diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml
index 02f1ef4b6da..7a100843f5e 100644
--- a/app/views/projects/pipelines/charts/_pipelines.haml
+++ b/app/views/projects/pipelines/charts/_pipelines.haml
@@ -14,19 +14,19 @@
.prepend-top-default
%p.light
- = _("Jobs for last week")
+ = _("Pipelines for last week")
(#{date_from_to(Date.today - 7.days, Date.today)})
%canvas#weekChart{ height: 200 }
.prepend-top-default
%p.light
- = _("Jobs for last month")
+ = _("Pipelines for last month")
(#{date_from_to(Date.today - 30.days, Date.today)})
%canvas#monthChart{ height: 200 }
.prepend-top-default
%p.light
- = _("Jobs for last year")
+ = _("Pipelines for last year")
%canvas#yearChart.padded{ height: 250 }
%script#pipelinesChartsData{ type: "application/json" }
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index c1729850cf4..9fc15d20f34 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -2,19 +2,23 @@
- page_title "Pipelines"
= render "projects/pipelines/head"
-#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
- "css-class" => container_class,
- "help-page-path" => help_page_path('ci/quick_start/README'),
- "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),
- "pending-path" => project_pipelines_path(@project, scope: :pending),
- "running-path" => project_pipelines_path(@project, scope: :running),
- "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" => @repository.gitlab_ci_yml,
- "ci-lint-path" => ci_lint_path } }
+%div{ 'class' => container_class }
+ - if show_auto_devops_callout?(@project)
+ = render 'shared/auto_devops_callout'
-= page_specific_javascript_bundle_tag('common_vue')
-= page_specific_javascript_bundle_tag('pipelines')
+ #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'),
+ "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),
+ "pending-path" => project_pipelines_path(@project, scope: :pending),
+ "running-path" => project_pipelines_path(@project, scope: :running),
+ "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?,
+ "ci-lint-path" => ci_lint_path } }
+
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('pipelines')
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 63f85fc69a2..7cc9fe79afd 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- add_to_breadcrumbs "Pipelines", project_pipelines_path(@project)
+- breadcrumb_title "##{@pipeline.id}"
- page_title "Pipeline"
= render "projects/pipelines/head"
diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml
index 3de518c8b9a..e8028059487 100644
--- a/app/views/projects/pipelines_settings/_badge.html.haml
+++ b/app/views/projects/pipelines_settings/_badge.html.haml
@@ -1,34 +1,32 @@
%div{ class: badge.title.gsub(' ', '-') }
- .col-lg-4.profile-settings-sidebar
- %h4.prepend-top-0
+ .col-lg-12
+ %h4
= badge.title.capitalize
- .col-lg-8
- .prepend-top-10
- .panel.panel-default
- .panel-heading
- %b
- = badge.title.capitalize
- &middot;
- = badge.to_html
- .pull-right
- = render 'shared/ref_switcher', destination: 'badges', align_right: true
- .panel-body
- .row
- .col-md-2.text-center
- Markdown
- .col-md-10.code.js-syntax-highlight
- = highlight('.md', badge.to_markdown)
- .row
- %hr
- .row
- .col-md-2.text-center
- HTML
- .col-md-10.code.js-syntax-highlight
- = highlight('.html', badge.to_html)
- .row
- %hr
- .row
- .col-md-2.text-center
- AsciiDoc
- .col-md-10.code.js-syntax-highlight
- = highlight('.adoc', badge.to_asciidoc)
+ .panel.panel-default
+ .panel-heading
+ %b
+ = badge.title.capitalize
+ &middot;
+ = badge.to_html
+ .pull-right
+ = render 'shared/ref_switcher', destination: 'badges', align_right: true
+ .panel-body
+ .row
+ .col-md-2.text-center
+ Markdown
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.md', badge.to_markdown)
+ .row
+ %hr
+ .row
+ .col-md-2.text-center
+ HTML
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.html', badge.to_html)
+ .row
+ %hr
+ .row
+ .col-md-2.text-center
+ AsciiDoc
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.adoc', badge.to_asciidoc)
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index 255d7ef38e0..324cd423ede 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -1,15 +1,43 @@
.row.prepend-top-default
- .col-lg-4.profile-settings-sidebar
- %h4.prepend-top-0
- Pipelines
- .col-lg-8
+ .col-lg-12
= form_for @project, url: project_pipelines_settings_path(@project) do |f|
%fieldset.builds-feature
- - unless @repository.gitlab_ci_yml
- .form-group
- %p Pipelines need to be configured before you can begin using Continuous Integration.
- = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
- %hr
+ .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.
+ = link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
+ = f.fields_for :auto_devops_attributes, @auto_devops do |form|
+ .radio
+ = form.label :enabled_true do
+ = form.radio_button :enabled, 'true'
+ %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.
+ .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.
+ .radio
+ = form.label :enabled do
+ = form.radio_button :enabled, nil
+ %strong
+ Instance default (status: #{current_application_settings.auto_devops_enabled?})
+ %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.
+ %br
+ %p
+ Define a domain used by Auto DevOps to deploy towards, this is required for deploys to succeed.
+ = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
+
+ %hr
.form-group.append-bottom-default
= f.label :runners_token, "Runner token", class: 'label-light'
= f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
@@ -60,8 +88,21 @@
= f.check_box :public_builds
%strong Public pipelines
.help-block
- Allow everyone to access pipelines for public and internal projects
+ Allow public access to pipelines and job details, including output logs and artifacts
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines'), target: '_blank'
+ .bs-callout.bs-callout-info
+ %p If enabled:
+ %ul
+ %li
+ For public projects, anyone can view pipelines and access job details (output logs and artifacts)
+ %li
+ For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)
+ %li
+ For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)
+ %p
+ If disabled, the access level will depend on the user's
+ permissions in the project.
+
%hr
.form-group
.checkbox
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index f6ca8d5a921..755128af565 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -8,7 +8,7 @@
= form_tag apply_import_project_project_members_path(@project), method: 'post', class: 'form-horizontal' do
.form-group
= label_tag :source_project_id, "Project", class: 'control-label'
- .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(current_user.authorized_projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true)
+ .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true)
.form-actions
= button_tag 'Import project members', class: "btn btn-create"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 9f7c5a315eb..25153fd0b6f 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,8 +1,5 @@
- page_title "Members"
-- if show_new_nav?
- - add_to_breadcrumbs("Settings", edit_project_path(@project))
-
.row.prepend-top-default
.col-lg-12
%h4
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index 0a5a38a3694..c786298e341 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- add_to_breadcrumbs "Tags", project_tags_path(@project)
+- breadcrumb_title @tag.name
- page_title "Edit", @tag.name, "Tags"
= render "projects/commits/head"
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index 2ef1f98ba48..ac8e15a48b2 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -7,6 +7,12 @@
= f.check_box :active
%span.light Paused Runners don't accept new jobs
.form-group
+ = label :protected, "Protected", class: 'control-label'
+ .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
+ .form-group
= label :run_untagged, 'Run untagged jobs', class: 'control-label'
.col-sm-10
.checkbox
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index abc97bcdff5..25d862ab4de 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -8,7 +8,7 @@
- if runner.locked?
= icon('lock', class: 'has-tooltip', title: 'Locked to current projects')
- %small
+ %small.edit-runner
= link_to edit_project_runner_path(@project, runner) do
%i.fa.fa-edit.btn
- else
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index 49415ba557b..dfab04aa1fb 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -20,6 +20,9 @@
%td Active
%td= @runner.active? ? 'Yes' : 'No'
%tr
+ %td Protected
+ %td= @runner.ref_protected? ? 'Yes' : 'No'
+ %tr
%td Can run untagged jobs
%td= @runner.run_untagged? ? 'Yes' : 'No'
%tr
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index 8056217bb1e..3e2a24a4c32 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -1,8 +1,6 @@
- breadcrumb_title "Integrations"
- page_title @service.title, "Services"
-
-- if show_new_nav?
- - add_to_breadcrumbs("Settings", edit_project_path(@project))
+- 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
index 15ba09b10ba..7d24c6a9122 100644
--- a/app/views/projects/settings/_head.html.haml
+++ b/app/views/projects/settings/_head.html.haml
@@ -23,7 +23,7 @@
= link_to project_settings_ci_cd_path(@project), title: 'Pipelines' do
%span
Pipelines
- - if Gitlab.config.pages.enabled
+ - if @project.pages_available?
= nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: 'Pages' do
%span
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 0c4130857da..d4f71d023c6 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -1,12 +1,54 @@
- @content_class = "limit-container-width" unless fluid_layout
-- page_title "Pipelines"
-
-- if show_new_nav?
- - add_to_breadcrumbs("Settings", edit_project_path(@project))
+- page_title "CI / CD Settings"
+- page_title "CI / CD"
= render "projects/settings/head"
-= render 'projects/runners/index'
-= render 'ci/variables/index'
-= render 'projects/triggers/index'
-= render 'projects/pipelines_settings/show'
+- expanded = Rails.env.test?
+
+%section.settings#js-general-pipeline-settings
+ .settings-header
+ %h4
+ General pipelines settings
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Update your CI/CD configuration, like job timeout.
+ .settings-content.no-animate{ class: ('expanded' if expanded) }
+ = render 'projects/pipelines_settings/show'
+
+%section.settings
+ .settings-header
+ %h4
+ Runners settings
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Register and see your runners for this project.
+ .settings-content.no-animate{ class: ('expanded' if expanded) }
+ = render 'projects/runners/index'
+
+%section.settings
+ .settings-header
+ %h4
+ Secret variables
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank'
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = render "ci/variables/content"
+ .settings-content.no-animate{ class: ('expanded' if expanded) }
+ = render 'ci/variables/index'
+
+%section.settings
+ .settings-header
+ %h4
+ Pipeline triggers
+ %button.btn.js-settings-toggle
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
+ impersonate their associated user including their access to projects and their project
+ permissions.
+ .settings-content.no-animate{ class: ('expanded' if expanded) }
+ = render 'projects/triggers/index'
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
index 149da96d3f6..933daa7f549 100644
--- a/app/views/projects/settings/integrations/show.html.haml
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -1,7 +1,6 @@
- @content_class = "limit-container-width" unless fluid_layout
+- breadcrumb_title "Integrations Settings"
- page_title 'Integrations'
-- if show_new_nav?
- - add_to_breadcrumbs("Settings", edit_project_path(@project))
= render "projects/settings/head"
= render 'projects/hooks/index'
= render 'projects/services/index'
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index cb37f3c7580..6d4af72b8ea 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -1,9 +1,7 @@
+- breadcrumb_title "Repository Settings"
- page_title "Repository"
- @content_class = "limit-container-width" unless fluid_layout
-- if show_new_nav?
- - add_to_breadcrumbs("Settings", edit_project_path(@project))
-
= render "projects/settings/head"
- content_for :page_specific_javascripts do
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index a9b39cedb1d..d8f5114f4b5 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- breadcrumb_title "Project"
+- breadcrumb_title "Details"
- @content_class = "limit-container-width" unless fluid_layout
= content_for :meta_tags do
@@ -27,9 +27,10 @@
= link_to project_tags_path(@project) do
#{n_('Tag', 'Tags', @repository.tag_count)} (#{number_with_delimiter(@repository.tag_count)})
- - if default_project_view != 'readme' && @repository.readme
+ - if @repository.readme
%li
- = link_to _('Readme'), readme_path(@project)
+ = link_to _('Readme'),
+ default_project_view != 'readme' ? readme_path(@project) : '#readme'
- if @repository.changelog
%li
@@ -81,5 +82,8 @@
- view_path = default_project_view
+ - if show_auto_devops_callout?(@project)
+ = render 'shared/auto_devops_callout'
+
%div{ class: project_child_container_class(view_path) }
= render view_path
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index d41cc8e0425..32844f5204a 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -1,3 +1,5 @@
+- add_to_breadcrumbs "Snippets", project_snippets_path(@project)
+- breadcrumb_title @snippet.to_reference
- page_title "Edit", "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
%h3.page-title
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index ccc5fe80755..65efc083fdd 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,15 +1,11 @@
- page_title "Snippets"
-- if show_new_nav? && can?(current_user, :create_project_snippet, @project)
- - content_for :breadcrumbs_extra do
- = link_to "New snippet", new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet"
-
- if current_user
.top-area
- include_private = @project.team.member?(current_user) || current_user.admin?
= render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
- .nav-controls{ class: ("visible-xs" if show_new_nav?) }
+ .nav-controls
- if can?(current_user, :create_project_snippet, @project)
= link_to "New snippet", new_project_snippet_path(@project), class: "btn btn-new", title: "New snippet"
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index d3e6b456f48..1359a815429 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -1,3 +1,5 @@
+- add_to_breadcrumbs "Snippets", project_snippets_path(@project)
+- breadcrumb_title "New"
- page_title "New Snippets"
%h3.page-title
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index d8e448dd2af..fda068f08c2 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,4 +1,6 @@
- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout
+- add_to_breadcrumbs "Snippets", dashboard_snippets_path
+- breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
= render 'shared/snippets/header'
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 00000e0667c..a6fe02fcae0 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -1,11 +1,9 @@
- @no_container = true
- @sort ||= sort_value_recently_updated
- page_title "Tags"
+- add_to_breadcrumbs("Repository", project_tree_path(@project))
= render "projects/commits/head"
-- if show_new_nav?
- - add_to_breadcrumbs("Repository", project_tree_path(@project))
-
.flex-list{ class: container_class }
.top-area.adjust
.nav-text.row-main-content
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index d02cd70f4c3..5d6eb4f4026 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- add_to_breadcrumbs "Tags", project_tags_path(@project)
+- breadcrumb_title @tag.name
- page_title @tag.name, "Tags"
= render "projects/commits/head"
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index 4579a912f39..4daacbe157c 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,5 +1,5 @@
- if readme.rich_viewer
- %article.file-holder.readme-holder{ class: ("limited-width-container" unless fluid_layout) }
+ %article.file-holder.readme-holder{ id: 'readme', class: ("limited-width-container" unless fluid_layout) }
.js-file-title.file-title
= blob_icon readme.mode, readme.name
= link_to project_blob_path(@project, tree_join(@ref, readme.path)) do
diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml
index f3d4706809f..abb3e918e87 100644
--- a/app/views/projects/tree/_tree_commit_column.html.haml
+++ b/app/views/projects/tree/_tree_commit_column.html.haml
@@ -1,2 +1,2 @@
%span.str-truncated
- = link_to_gfm commit.full_title, project_commit_path(@project, commit.id), class: "tree-commit-link"
+ = link_to_markdown commit.full_title, project_commit_path(@project, commit.id), class: "tree-commit-link"
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 427b059cb82..853e2a6e7ec 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -2,8 +2,9 @@
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path
- if show_new_repo?
- = icon('long-arrow-right', title: 'to target branch')
- = render 'shared/target_switcher', destination: 'tree', path: @path
+ .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/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index 0c9c8750f2c..56197382a70 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -1,7 +1,7 @@
%tr{ class: "tree-item #{tree_hex_class(tree_item)}" }
%td.tree-item-file-name
= tree_icon(type, tree_item.mode, tree_item.name)
- - path = flatten_tree(tree_item)
+ - path = flatten_tree(@path, tree_item)
= link_to project_tree_path(@project, tree_join(@id || @commit.id, path)), title: path do
%span.str-truncated= path
%td.hidden-xs.tree-commit
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 375e6764add..d84a1fd7ee1 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -14,5 +14,7 @@
= render "projects/commits/head"
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
+ - if show_auto_devops_callout?(@project)
+ = 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/triggers/_content.html.haml b/app/views/projects/triggers/_content.html.haml
index ea32eac2ae2..6c2d603d95d 100644
--- a/app/views/projects/triggers/_content.html.haml
+++ b/app/views/projects/triggers/_content.html.haml
@@ -1,14 +1,8 @@
-%h4.prepend-top-0
- Triggers
-%p.prepend-top-20
- Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
- impersonate their associated user including their access to projects and their project
- permissions.
-%p.prepend-top-20
+%p.append-bottom-default
Triggers with the
%span.label.label-primary legacy
label do not have an associated user and only have access to the current project.
-%p.append-bottom-0
+ %br
= succeed '.' do
Learn more in the
= link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml
index e9a2f803edd..0f655e4ed83 100644
--- a/app/views/projects/triggers/_index.html.haml
+++ b/app/views/projects/triggers/_index.html.haml
@@ -1,7 +1,6 @@
.row.prepend-top-default.append-bottom-default.triggers-container
- .col-lg-4
+ .col-lg-12
= render "projects/triggers/content"
- .col-lg-8
.panel.panel-default
.panel-heading
%h4.panel-title
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index dece1fad0bb..d533c611a38 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- add_to_breadcrumbs "Wiki", get_project_wiki_path(@project)
+- breadcrumb_title "Pages"
- page_title "Pages", "Wiki"
%div{ class: container_class }
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 9dadd685ea2..b066a812ec8 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,14 +1,13 @@
- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout
-- breadcrumb_title "Wiki"
+- 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)
.wiki-page-header.has-sidebar-toggle
%button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
- .wiki-breadcrumb
- %span= breadcrumb(@page.slug)
-
.nav-text
%h2.wiki-page-title= @page.title.capitalize
%span.wiki-last-edit-by
diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml
index 3139be1cd37..a4a5cec1314 100644
--- a/app/views/search/_form.html.haml
+++ b/app/views/search/_form.html.haml
@@ -11,5 +11,5 @@
%span.sr-only
Clear search
- unless params[:snippets].eql? 'true'
- = render 'filter' if current_user
+ = render 'filter'
= button_tag "Search", class: "btn btn-success btn-search"
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
new file mode 100644
index 00000000000..2f09c2fec87
--- /dev/null
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -0,0 +1,15 @@
+.user-callout{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
+ .bordered-box.landing.content-block
+ %button.btn.btn-default.close.js-close-callout{ type: 'button',
+ 'aria-label' => 'Dismiss Auto DevOps box' }
+ = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
+ .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.')
+ %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_to _('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/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index e50ab5fea09..151aad306a0 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,11 +1,11 @@
%ul.nav-links.event-filter.scrolling-tabs
- = event_filter_link EventFilter.all, 'All'
+ = event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all')
- if event_filter_visible(:repository)
- = event_filter_link EventFilter.push, 'Push events'
+ = event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events')
- if event_filter_visible(:merge_requests)
- = event_filter_link EventFilter.merged, 'Merge events'
+ = event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events')
- if event_filter_visible(:issues)
- = event_filter_link EventFilter.issue, 'Issue events'
+ = event_filter_link EventFilter.issue, _('Issue events'), s_('EventFilterBy|Filter by issue events')
- if comments_visible?
- = event_filter_link EventFilter.comments, 'Comments'
- = event_filter_link EventFilter.team, 'Team'
+ = event_filter_link EventFilter.comments, _('Comments'), s_('EventFilterBy|Filter by comments')
+ = event_filter_link EventFilter.team, _('Team'), s_('EventFilterBy|Filter by team')
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index 8d5b5129454..2e1bd5a088c 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -1,6 +1,6 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('group')
-- parent = GroupFinder.new(current_user).execute(id: params[:parent_id] || @group.parent_id)
+- parent = @group.parent
- group_path = root_url
- group_path << parent.full_path + '/' if parent
@@ -13,13 +13,12 @@
%span>= root_url
- if parent
%strong= parent.full_path + '/'
+ = f.hidden_field :parent_id
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
title: 'Please choose a group path with no special characters.',
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
- - if parent
- = f.hidden_field :parent_id, value: parent.id
- if @group.persisted?
.alert.alert-warning.prepend-top-10
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 873179339dc..233d8c95eda 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -13,4 +13,4 @@
%li
The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
%li
- To migrate an SVN repository, check out #{link_to "this document", help_page_path('workflow/importing/migrating_from_svn')}.
+ To migrate an SVN repository, check out #{link_to "this document", help_page_path('user/project/import/svn')}.
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 8ded7440de3..23a418ad640 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -14,10 +14,10 @@
%ul
%li
= link_to_label(label, subject: subject, type: :merge_request) do
- view merge requests
+ View merge requests
%li
= link_to_label(label, subject: subject) do
- view open issues
+ View open issues
- if current_user
%li.label-subscription
- if can_subscribe_to_label_in_different_levels?(label)
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index 10e6c49ae9f..0ef9de5fed6 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,4 +1,4 @@
-<svg width="28" height="28" class="tanuki-logo" viewBox="0 0 36 36">
+<svg width="24" height="24" class="tanuki-logo" viewBox="0 0 36 36">
<path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
<path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
<path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index 96502d7ce93..ac2ebb701a5 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,6 +1,6 @@
- if any_projects?(@projects)
- .project-item-select-holder.btn-group.pull-right
- %a.btn.btn-new.new-project-item-link{ href: '', data: { label: local_assigns[:label] } }
+ .project-item-select-holder.btn-group
+ %a.btn.btn-new.new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
= icon('spinner spin')
= project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled]
%button.btn.btn-new.new-project-item-select-button
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 4498c8f8349..7ad743b3b81 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -6,7 +6,7 @@
- @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), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" }
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags")
diff --git a/app/views/shared/_target_switcher.html.haml b/app/views/shared/_target_switcher.html.haml
index 3672b552f10..9236868652f 100644
--- a/app/views/shared/_target_switcher.html.haml
+++ b/app/views/shared/_target_switcher.html.haml
@@ -1,5 +1,5 @@
- dropdown_toggle_text = @ref || @project.default_branch
-= form_tag nil, method: :get, class: "project-refs-target-form" do
+= 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
diff --git a/app/views/shared/_user_dropdown_experimental_features.html.haml b/app/views/shared/_user_dropdown_experimental_features.html.haml
deleted file mode 100644
index 8e71407b748..00000000000
--- a/app/views/shared/_user_dropdown_experimental_features.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-%li= link_to 'Experimental features', profile_preferences_path(anchor: 'experimental-features')
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index 73efec88bb1..192d2502aaf 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -1,6 +1,6 @@
- with_label = local_assigns.fetch(:with_label, true)
-.form-group.project-visibility-level-holder
+.form-group.visibility-level-setting
- if with_label
= f.label :visibility_level, class: 'control-label' do
Visibility Level
diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml
index 182c4eebd50..0ec7677a566 100644
--- a/app/views/shared/_visibility_radios.html.haml
+++ b/app/views/shared/_visibility_radios.html.haml
@@ -1,15 +1,17 @@
- Gitlab::VisibilityLevel.values.each do |level|
- - next if skip_level?(form_model, level)
- .radio
- - restricted = restricted_visibility_levels.include?(level)
+ - disallowed = disallowed_visibility_level?(form_model, level)
+ - restricted = restricted_visibility_levels.include?(level)
+ - disabled = disallowed || restricted
+ .radio{ class: [('disabled' if disabled), ('restricted' if restricted)] }
= form.label "#{model_method}_#{level}" do
- = form.radio_button model_method, level, checked: (selected_level == level), disabled: restricted
+ = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled
= visibility_level_icon(level)
.option-title
= visibility_level_label(level)
- .option-descr
+ .option-description
= visibility_level_description(level, form_model)
-- unless restricted_visibility_levels.empty?
- %div
- %span.info
- Some visibility level settings have been restricted by the administrator.
+ .option-disabled-reason
+ - if restricted
+ = restricted_visibility_level_description(level)
+ - elsif disallowed
+ = disallowed_visibility_level_description(level, form_model)
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
new file mode 100644
index 00000000000..1a50b7d4b69
--- /dev/null
+++ b/app/views/shared/boards/_show.html.haml
@@ -0,0 +1,40 @@
+- @no_breadcrumb_container = true
+- @no_container = true
+- @content_class = "issue-boards-content"
+- breadcrumb_title "Issue Board"
+- page_title "Boards"
+
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'filtered_search'
+ = webpack_bundle_tag 'boards'
+
+ %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"
+
+.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-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ %board{ "v-cloak" => true,
+ "v-for" => "list in state.lists",
+ "ref" => "board",
+ ":list" => "list",
+ ":disabled" => "disabled",
+ ":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
+ ":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) }
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
new file mode 100644
index 00000000000..ce0aa72ab00
--- /dev/null
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -0,0 +1,47 @@
+.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded }',
+ ":data-id" => "list.id" }
+ .board-inner
+ %header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" }
+ %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' }
+ %i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable",
+ ":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded && list.position === -1, \"fa-caret-left\": !list.isExpanded && list.position !== -1 }",
+ "aria-hidden": "true" }
+
+ %span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"",
+ ":title" => '(list.label ? list.label.description : "")' }
+ {{ list.title }}
+
+ %span.has-tooltip{ "v-if": "list.type === \"label\"",
+ ":title" => '(list.label ? list.label.description : "")',
+ data: { container: "body", placement: "bottom" },
+ class: "label color-label title board-title-text",
+ ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.color ? list.label.text_color : \"#2e2e2e\") }" }
+ {{ list.title }}
+ - if can?(current_user, :admin_list, current_board_parent)
+ %board-delete{ "inline-template" => true,
+ ":list" => "list",
+ "v-if" => "!list.preset && list.id" }
+ %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
+ = icon("trash")
+ .issue-count-badge.clearfix{ "v-if" => 'list.type !== "blank"' }
+ %span.issue-count-badge-count.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",
+ "@click" => "showNewIssueForm",
+ "v-if" => 'list.type !== "closed"',
+ "aria-label" => "New issue",
+ "title" => "New issue",
+ data: { placement: "top", container: "body" } }
+ = icon("plus", class: "js-no-trigger-collapse")
+
+ %board-list{ "v-if" => 'list.type !== "blank"',
+ ":list" => "list",
+ ":issues" => "list.issues",
+ ":loading" => "list.loading",
+ ":disabled" => "disabled",
+ ":issue-link-base" => "issueLinkBase",
+ ":root-path" => "rootPath",
+ "ref" => "board-list" }
+ - if can?(current_user, :admin_list, current_board_parent)
+ %board-blank-state{ "v-if" => 'list.id == "blank"' }
diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml
new file mode 100644
index 00000000000..b3f73e96b81
--- /dev/null
+++ b/app/views/shared/boards/components/_sidebar.html.haml
@@ -0,0 +1,28 @@
+%board-sidebar{ "inline-template" => true,
+ ":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" }
+ %transition{ name: "boards-sidebar-slide" }
+ %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
+ .issuable-sidebar
+ .block.issuable-sidebar-header
+ %span.issuable-header-text.hide-collapsed.pull-left
+ %strong
+ {{ issue.title }}
+ %br/
+ %span
+ = precede "#" do
+ {{ issue.iid }}
+ %a.gutter-toggle.pull-right{ role: "button",
+ href: "#",
+ "@click.prevent" => "closeSidebar",
+ "aria-label" => "Toggle sidebar" }
+ = custom_icon("icon_close", size: 15)
+ .js-issuable-update
+ = render "shared/boards/components/sidebar/assignee"
+ = render "shared/boards/components/sidebar/milestone"
+ = render "shared/boards/components/sidebar/due_date"
+ = render "shared/boards/components/sidebar/labels"
+ = render "shared/boards/components/sidebar/notifications"
+ %remove-btn{ ":issue" => "issue",
+ ":issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'",
+ ":list" => "list",
+ "v-if" => "canRemove" }
diff --git a/app/views/shared/boards/components/sidebar/_assignee.html.haml b/app/views/shared/boards/components/sidebar/_assignee.html.haml
new file mode 100644
index 00000000000..3d2e8471a60
--- /dev/null
+++ b/app/views/shared/boards/components/sidebar/_assignee.html.haml
@@ -0,0 +1,32 @@
+.block.assignee{ ref: "assigneeBlock" }
+ %template{ "v-if" => "issue.assignees" }
+ %assignee-title{ ":number-of-assignees" => "issue.assignees.length",
+ ":loading" => "loadingAssignees",
+ ":editable" => can_admin_issue? }
+ %assignees.value{ "root-path" => "#{root_url}",
+ ":users" => "issue.assignees",
+ ":editable" => can_admin_issue?,
+ "@assign-self" => "assignSelf" }
+
+ - if can_admin_issue?
+ .selectbox.hide-collapsed
+ %input.js-vue{ type: "hidden",
+ name: "issue[assignee_ids][]",
+ ":value" => "assignee.id",
+ "v-if" => "issue.assignees",
+ "v-for" => "assignee in issue.assignees",
+ ":data-avatar_url" => "assignee.avatar",
+ ":data-name" => "assignee.name",
+ ":data-username" => "assignee.username" }
+ .dropdown
+ - dropdown_options = issue_assignees_dropdown_options
+ %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: board_sidebar_user_data,
+ ":data-issuable-id" => "issue.iid",
+ ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
+ = dropdown_options[:title]
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
+ = dropdown_title("Assign to")
+ = dropdown_filter("Search users")
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/shared/boards/components/sidebar/_due_date.html.haml b/app/views/shared/boards/components/sidebar/_due_date.html.haml
new file mode 100644
index 00000000000..db794d6f855
--- /dev/null
+++ b/app/views/shared/boards/components/sidebar/_due_date.html.haml
@@ -0,0 +1,32 @@
+.block.due_date
+ .title
+ Due date
+ - if can_admin_issue?
+ = icon("spinner spin", class: "block-loading")
+ = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
+ .value
+ .value-content
+ %span.no-value{ "v-if" => "!issue.dueDate" }
+ No due date
+ %span.bold{ "v-if" => "issue.dueDate" }
+ {{ issue.dueDate | due-date }}
+ - if can_admin_issue?
+ %span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
+ \-
+ %a.js-remove-due-date{ href: "#", role: "button" }
+ remove due date
+ - if can_admin_issue?
+ .selectbox
+ %input{ type: "hidden",
+ name: "issue[due_date]",
+ ":value" => "issue.dueDate" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
+ data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" },
+ ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
+ %span.dropdown-toggle-text Due date
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-menu-due-date
+ = dropdown_title('Due date')
+ = dropdown_content do
+ .js-due-date-calendar
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
new file mode 100644
index 00000000000..1f540bdaf93
--- /dev/null
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -0,0 +1,37 @@
+.block.labels
+ .title
+ Labels
+ - if can_admin_issue?
+ = icon("spinner spin", class: "block-loading")
+ = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
+ .value.issuable-show-labels
+ %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
+ None
+ %a{ href: "#",
+ "v-for" => "label in issue.labels" }
+ %span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
+ {{ label.title }}
+ - if can_admin_issue?
+ .selectbox
+ %input{ type: "hidden",
+ name: "issue[label_names][]",
+ "v-for" => "label in issue.labels",
+ ":value" => "label.id" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
+ data: { toggle: "dropdown",
+ field_name: "issue[label_names][]",
+ show_no: "true",
+ show_any: "true",
+ project_id: @project&.try(:id),
+ labels: labels_filter_path(false),
+ namespace_path: @project.try(:namespace).try(:full_path),
+ project_path: @project.try(:path) },
+ ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
+ %span.dropdown-toggle-text
+ Label
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ = render partial: "shared/issuable/label_page_default"
+ - if can?(current_user, :admin_label, current_board_parent)
+ = render partial: "shared/issuable/label_page_create"
diff --git a/app/views/shared/boards/components/sidebar/_milestone.html.haml b/app/views/shared/boards/components/sidebar/_milestone.html.haml
new file mode 100644
index 00000000000..d09c7c218e0
--- /dev/null
+++ b/app/views/shared/boards/components/sidebar/_milestone.html.haml
@@ -0,0 +1,29 @@
+.block.milestone
+ .title
+ Milestone
+ - if can_admin_issue?
+ = icon("spinner spin", class: "block-loading")
+ = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
+ .value
+ %span.no-value{ "v-if" => "!issue.milestone" }
+ None
+ %span.bold.has-tooltip{ "v-if" => "issue.milestone" }
+ {{ issue.milestone.title }}
+ - if can_admin_issue?
+ .selectbox
+ %input{ type: "hidden",
+ ":value" => "issue.milestone.id",
+ name: "issue[milestone_id]",
+ "v-if" => "issue.milestone" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", milestones: milestones_filter_path(format: :json), ability_name: "issue", use_id: "true", default_no: "true" },
+ ":data-selected" => "milestoneTitle",
+ ":data-issuable-id" => "issue.iid",
+ ":data-issue-update" => "'#{build_issue_link_base}/' + issue.iid + '.json'" }
+ Milestone
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ = dropdown_title("Assign milestone")
+ = dropdown_filter("Search milestones")
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/shared/boards/components/sidebar/_notifications.html.haml b/app/views/shared/boards/components/sidebar/_notifications.html.haml
new file mode 100644
index 00000000000..9b989c23cab
--- /dev/null
+++ b/app/views/shared/boards/components/sidebar/_notifications.html.haml
@@ -0,0 +1,7 @@
+- if current_user
+ .block.light.subscription{ ":data-url" => "'#{build_issue_link_base}/' + issue.iid + '/toggle_subscription'" }
+ %span.issuable-header-text.hide-collapsed.pull-left
+ Notifications
+ %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
+ %span
+ {{issue.subscribed ? 'Unsubscribe' : 'Subscribe'}}
diff --git a/app/views/shared/boards/index.html.haml b/app/views/shared/boards/index.html.haml
new file mode 100644
index 00000000000..2a5b8b1441e
--- /dev/null
+++ b/app/views/shared/boards/index.html.haml
@@ -0,0 +1 @@
+= render "show"
diff --git a/app/views/shared/boards/show.html.haml b/app/views/shared/boards/show.html.haml
new file mode 100644
index 00000000000..2a5b8b1441e
--- /dev/null
+++ b/app/views/shared/boards/show.html.haml
@@ -0,0 +1 @@
+= render "show"
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index b0c0ab523c7..68737e8da66 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -15,7 +15,7 @@
Issues can be bugs, tasks or ideas to be discussed.
Also, issues are searchable and filterable.
- if project_select_button
- = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
+ = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues
- else
= link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
- else
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index 3e64f403b8b..ff5741b6d61 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -14,7 +14,7 @@
%p
Interested parties can even contribute by pushing commits if they want to.
- if project_select_button
- = render 'shared/new_project_item_select', path: 'merge_requests/new', label: 'New merge request'
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: 'New merge request', type: :merge_requests
- else
= link_to 'New merge request', button_path, class: 'btn btn-new', title: 'New merge request', id: 'new_merge_request_link'
- else
diff --git a/app/views/shared/icons/_caret_down.svg b/app/views/shared/icons/_caret_down.svg
new file mode 100644
index 00000000000..fd80fd0f651
--- /dev/null
+++ b/app/views/shared/icons/_caret_down.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="caret-down" viewBox="0 0 16 16"><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"/></svg>
diff --git a/app/views/shared/icons/_ellipsis_v.svg b/app/views/shared/icons/_ellipsis_v.svg
new file mode 100644
index 00000000000..9117a9bb9ec
--- /dev/null
+++ b/app/views/shared/icons/_ellipsis_v.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1600"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></svg>
diff --git a/app/views/shared/icons/_node_express.svg b/app/views/shared/icons/_express.svg
index f2c94319f19..f2c94319f19 100644
--- a/app/views/shared/icons/_node_express.svg
+++ b/app/views/shared/icons/_express.svg
diff --git a/app/views/shared/icons/_icon_arrow_circle_o_right.svg b/app/views/shared/icons/_icon_arrow_circle_o_right.svg
index 5e45c6c15ce..29bdc8e5754 100644
--- a/app/views/shared/icons/_icon_arrow_circle_o_right.svg
+++ b/app/views/shared/icons/_icon_arrow_circle_o_right.svg
@@ -1 +1,2 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><g fill-rule="evenodd"><path fill-rule="nonzero" d="m0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7m1 0c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6"/><path d="m7 6h-2.702c-.154 0-.298.132-.298.295v1.41c0 .164.133.295.298.295h2.702v1.694c0 .18.095.209.213.09l2.539-2.568c.115-.116.118-.312 0-.432l-2.539-2.568c-.115-.116-.213-.079-.213.09v1.694"/></g></svg>
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m9 6h-7a2 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 -1.707.707z" fill-rule="evenodd"/></svg>
+
diff --git a/app/views/shared/icons/_icon_arrow_right.svg.erb b/app/views/shared/icons/_icon_arrow_right.svg.erb
new file mode 100644
index 00000000000..24d64eb73bd
--- /dev/null
+++ b/app/views/shared/icons/_icon_arrow_right.svg.erb
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><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"/></svg>
diff --git a/app/views/shared/icons/_icon_autodevops.svg b/app/views/shared/icons/_icon_autodevops.svg
new file mode 100644
index 00000000000..807ff27bb67
--- /dev/null
+++ b/app/views/shared/icons/_icon_autodevops.svg
@@ -0,0 +1,54 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="189" height="179" viewBox="0 0 189 179">
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#FFFFFF" fill-rule="nonzero" d="M110.160166,47.6956996 L160.160166,47.6956996 C165.683013,47.6956996 170.160166,52.1728521 170.160166,57.6956996 L170.160166,117.6957 C170.160166,123.218547 165.683013,127.6957 160.160166,127.6957 L110.160166,127.6957 C104.637318,127.6957 100.160166,123.218547 100.160166,117.6957 L100.160166,57.6956996 C100.160166,52.1728521 104.637318,47.6956996 110.160166,47.6956996 Z" transform="rotate(10 135.16 87.696)"/>
+ <path fill="#EEEEEE" fill-rule="nonzero" d="M110.160166,51.6956996 C106.846457,51.6956996 104.160166,54.3819911 104.160166,57.6956996 L104.160166,117.6957 C104.160166,121.009408 106.846457,123.6957 110.160166,123.6957 L160.160166,123.6957 C163.473874,123.6957 166.160166,121.009408 166.160166,117.6957 L166.160166,57.6956996 C166.160166,54.3819911 163.473874,51.6956996 160.160166,51.6956996 L110.160166,51.6956996 Z M110.160166,47.6956996 L160.160166,47.6956996 C165.683013,47.6956996 170.160166,52.1728521 170.160166,57.6956996 L170.160166,117.6957 C170.160166,123.218547 165.683013,127.6957 160.160166,127.6957 L110.160166,127.6957 C104.637318,127.6957 100.160166,123.218547 100.160166,117.6957 L100.160166,57.6956996 C100.160166,52.1728521 104.637318,47.6956996 110.160166,47.6956996 Z" transform="rotate(10 135.16 87.696)"/>
+ <rect width="70" height="80" x="80" y="34" fill="#000000" fill-opacity=".03" transform="rotate(5 115 74)" rx="10"/>
+ <path fill="#000000" fill-opacity=".03" fill-rule="nonzero" d="M126.028,178 L150,178 C169.972,177.457 186,161.1 186,141 C186,120.565 169.435,104 149,104 C148.007,104 147.023,104.04 146.05,104.116 C141.1,89.51 127.277,79 111,79 C105.71511,78.9924557 100.490375,80.1212986 95.68,82.31 C89.03,72.47 77.77,66 65,66 C44.565,66 28,82.565 28,103 C28,103.7 28.02,104.397 28.058,105.088 C11.944,109.088 0,123.648 0,141 C0,161.435 16.565,178 37,178 C37.324,178 37.646,177.996 37.968,177.988 L126.028,178 Z"/>
+ <g transform="rotate(5 -300.07 1010.998)">
+ <rect width="70" height="80" fill="#FFFFFF" rx="10"/>
+ <path fill="#EEEEEE" fill-rule="nonzero" d="M10,4 C6.6862915,4 4,6.6862915 4,10 L4,70 C4,73.3137085 6.6862915,76 10,76 L60,76 C63.3137085,76 66,73.3137085 66,70 L66,10 C66,6.6862915 63.3137085,4 60,4 L10,4 Z M10,-2.68673972e-14 L60,-2.68673972e-14 C65.5228475,-2.78819278e-14 70,4.4771525 70,10 L70,70 C70,75.5228475 65.5228475,80 60,80 L10,80 C4.4771525,80 9.15550472e-14,75.5228475 9.08786935e-14,70 L9.08786935e-14,10 C9.02023397e-14,4.4771525 4.4771525,-2.58528666e-14 10,-2.68673972e-14 Z"/>
+ <rect width="8" height="4" x="14" y="18" fill="#FC6D26" rx="2"/>
+ <rect width="8" height="4" x="32" y="38" fill="#E1DBF1" rx="2"/>
+ <rect width="8" height="4" x="45" y="48" fill="#FEE1D3" rx="2"/>
+ <rect width="8" height="4" x="44" y="38" fill="#FEF0E8" rx="2"/>
+ <rect width="12" height="4" x="26" y="18" fill="#EFEDF8" rx="2"/>
+ <rect width="6" height="4" x="15" y="48" fill="#FEF0E8" rx="2"/>
+ <rect width="6" height="4" x="25" y="48" fill="#FC6D26" rx="2"/>
+ <rect width="6" height="4" x="35" y="48" fill="#EEEEEE" rx="2"/>
+ <rect width="12" height="4" x="14" y="28" fill="#EEEEEE" rx="2"/>
+ <rect width="12" height="4" x="44" y="58" fill="#6B4FBB" rx="2"/>
+ <rect width="26" height="4" x="30" y="28" fill="#C3B8E3" rx="2"/>
+ <rect width="26" height="4" x="15" y="58" fill="#FEE1D3" rx="2"/>
+ <rect width="14" height="4" x="42" y="18" fill="#E1DBF1" rx="2"/>
+ <rect width="14" height="4" x="14" y="38" fill="#6B4FBB" rx="2"/>
+ </g>
+ <g transform="translate(3 67)">
+ <path fill="#FFFFFF" fill-rule="nonzero" d="M126.028,112 L150,112 C169.972,111.457 186,95.1 186,75 C186,54.565 169.435,38 149,38 C148.007,38 147.023,38.04 146.05,38.116 C141.1,23.51 127.277,13 111,13 C105.71511,12.9924557 100.490375,14.1212986 95.68,16.31 C89.03,6.47 77.77,0 65,0 C44.565,0 28,16.565 28,37 C28,37.7 28.02,38.397 28.058,39.088 C11.944,43.088 0,57.648 0,75 C0,95.435 16.565,112 37,112 C37.324,112 37.646,111.996 37.968,111.988 L126.028,112 Z"/>
+ <path fill="#FFFFFF" d="M126.028,110 L149.946,110 C168.876,109.486 184,93.976 184,75 C184,55.67 168.33,40 149,40 C148.064,40 147.133,40.037 146.207,40.11 L144.656,40.232 L144.156,38.758 C139.381,24.67 126.116,15 111,15 C106.001056,14.9926759 101.058995,16.0604828 96.509,18.131 L94.969,18.832 L94.022,17.431 C87.552,7.855 76.774,2 65,2 C45.67,2 30,17.67 30,37 C30,37.661 30.018,38.32 30.055,38.977 L30.147,40.63 L28.54,41.029 C13.06,44.87 2,58.827 2,75 C2,94.33 17.67,110 37,110 C37.306,110 37.612,109.996 37.968,109.988 L126.028,110 Z"/>
+ <path fill="#EEEEEE" fill-rule="nonzero" d="M149,38 C169.434569,38 186,54.5654305 186,75 C186,95.0477955 170.024944,111.45554 149.946,112 L126.028,112 L38.0129325,111.987495 C37.6348039,111.995992 37.3157048,112 37,112 C16.5654305,112 0,95.4345695 0,75 C0,57.9772494 11.6188339,43.1669444 28.0580557,39.0879359 C28.0192558,38.39805 28,37.7022134 28,37 C28,16.5654305 44.5654305,0 65,0 C77.3556804,0 88.7905015,6.1156181 95.6789703,16.310978 C100.491636,14.1213221 105.717209,12.9922579 111,13 C126.893041,13 140.974134,23.139867 146.049999,38.1155308 C147.031471,38.0388091 148.014765,38 149,38 Z M149.891715,108.000737 C167.750695,107.515818 182,92.8805667 182,75 C182,56.7745695 167.225431,42 149,42 C148.1197,42 147.241142,42.0346799 146.363833,42.1038413 L144.812833,42.2258413 C143.900567,42.2975992 143.055957,41.7410534 142.762001,40.8744692 L142.261844,39.400007 C137.735045,26.0442906 125.175364,17 110.99707,16.9999979 C106.284902,16.9930939 101.626355,17.9996435 97.3375854,19.9512874 L95.7975854,20.6522874 C94.9091924,21.0566793 93.8586575,20.7607079 93.3120297,19.952022 L92.3648004,18.5506827 C86.2175802,9.45241684 76.0227954,4 65,4 C46.7745695,4 32,18.7745695 32,37 C32,37.6282966 32.0172077,38.249659 32.0519095,38.8658592 L32.1439095,40.5188592 C32.1972908,41.4779816 31.5612439,42.3395846 30.6289443,42.5710641 L29.0216479,42.9701376 C14.3638424,46.6071293 4,59.8177208 4,75 C4,93.2254305 18.7745695,108 37,108 C37.2837952,108 37.5733598,107.996363 37.9682725,107.988 L126.02825,108 L149.891715,108.000737 Z"/>
+ </g>
+ <g fill-rule="nonzero" transform="rotate(15 -315.035 277.714)">
+ <path fill="#FFFFFF" d="M12.275,10.57 C13.986216,9.15630755 15.921048,8.03765363 18,7.26 L18,5.5 C18,2.463 20.47,0 23.493,0 L26.507,0 C27.9648848,0.000530018716 29.3628038,0.580386367 30.3930274,1.61192286 C31.4232511,2.64345935 32.0013267,4.04211574 32,5.5 L32,7.26 C34.098,8.043 36.03,9.17 37.725,10.57 L39.253,9.688 C41.8816141,8.17268496 45.2407537,9.07039379 46.763,11.695 L48.27,14.305 C48.9984289,15.5678669 49.1951495,17.0684426 48.8168566,18.4763972 C48.4385638,19.8843518 47.5162683,21.0842673 46.253,21.812 L44.728,22.693 C44.907,23.769 45,24.873 45,26 C45,27.127 44.907,28.231 44.728,29.307 L46.253,30.187 C48.8800379,31.705769 49.7822744,35.0642181 48.27,37.695 L46.763,40.305 C46.0335844,41.5673849 44.8323832,42.4881439 43.4238487,42.8645658 C42.0153143,43.2409877 40.5149245,43.0422119 39.253,42.312 L37.725,41.43 C36.013784,42.8436924 34.078952,43.9623464 32,44.74 L32,46.5 C32,49.537 29.53,52 26.507,52 L23.493,52 C22.0351152,51.99947 20.6371962,51.4196136 19.6069726,50.3880771 C18.5767489,49.3565406 17.9986733,47.9578843 18,46.5 L18,44.74 C15.921048,43.9623464 13.986216,42.8436924 12.275,41.43 L10.747,42.312 C8.11838594,43.827315 4.75924629,42.9296062 3.237,40.305 L1.73,37.695 C1.00157113,36.4321331 0.804850523,34.9315574 1.18314337,33.5236028 C1.56143621,32.1156482 2.48373172,30.9157327 3.747,30.188 L5.272,29.307 C5.09051204,28.2140265 4.9995366,27.107939 5,26 C5,24.873 5.093,23.769 5.272,22.693 L3.747,21.813 C1.11996213,20.294231 0.217725591,16.9357819 1.73,14.305 L3.237,11.695 C3.96641559,10.4326151 5.16761682,9.51185609 6.57615125,9.13543417 C7.98468568,8.75901226 9.48507553,8.95778814 10.747,9.688 L12.275,10.57 Z"/>
+ <path fill="#E1DBF1" d="M17.9996486,7.25963195 L18.0000013,5.49772675 C18.0034459,2.46713881 20.4561478,0.00952173148 23.493,0 L26.507,0 C29.542757,0 32,2.46161709 32,5.5 L32,7.25850184 C34.0799663,8.03664754 36.0149544,9.15559094 37.7260175,10.5694605 L39.2547869,9.68691874 C41.8812087,8.17416302 45.2363972,9.06948854 46.7630175,11.6949424 L48.270687,14.3061027 C48.9989901,15.569417 49.1952874,17.0704122 48.816349,18.4785295 C48.4374106,19.8866468 47.5143145,21.0864021 46.2530682,21.8120114 L44.7278655,22.6926677 C44.9091017,23.7802451 45,24.8850821 45,26 C45,27.1144218 44.9091826,28.218078 44.7278653,29.3073326 L46.2547984,30.1889888 C48.8778516,31.7070439 49.7801588,35.0599752 48.2700175,37.6950576 L46.7625317,40.3058986 C46.0327098,41.5684739 44.8309328,42.4891542 43.4219037,42.8651509 C42.0128746,43.2411475 40.512172,43.0416186 39.2533538,42.312255 L37.7244858,41.4299789 C36.013753,42.8435912 34.0794396,43.9622923 32.0003514,44.7403681 L31.9999987,46.5022733 C31.9965541,49.5328612 29.5438522,51.9904783 26.507,52 L23.493,52 C20.457243,52 18,49.5383829 18,46.5 L18,44.7414988 C15.9200337,43.9633525 13.9850456,42.8444091 12.2739825,41.4305395 L10.7452131,42.3130813 C8.11879127,43.825837 4.76360277,42.9305115 3.23698247,40.3050576 L1.72931303,37.6938973 C1.0010099,36.430583 0.804712603,34.9295878 1.18365098,33.5214705 C1.56258936,32.1133532 2.48568546,30.9135979 3.74693178,30.1879886 L5.27213454,29.3073323 C5.09089825,28.2197549 5,27.114918 5.00000019,26.0008761 C4.99951488,24.8930059 5.0904571,23.7869854 5.27213502,22.6926675 L3.74520157,21.8110112 C1.12214836,20.2929561 0.219841192,16.9400248 1.72998247,14.3049424 L3.23746831,11.6941014 C3.96729024,10.4315261 5.16906725,9.51084579 6.5780963,9.13484913 C7.98712536,8.75885247 9.48782803,8.95838137 10.7466462,9.687745 L12.2748018,10.56961 C14.0209791,9.13635584 15.9392199,8.03072455 17.9996486,7.25963195 Z M13.7518374,14.537862 C13.108069,15.069723 12.2016163,15.1456339 11.4783538,14.728255 L8.74433999,13.1505123 C8.40103903,12.9516035 7.99274958,12.8973186 7.60940137,12.9996143 C7.22605315,13.10191 6.89909107,13.3523954 6.70101753,13.6950576 L5.19724591,16.2994454 C4.78547321,17.0179634 5.03203388,17.9341714 5.74706822,18.3479886 L8.47306822,19.9219886 C9.19530115,20.3390079 9.58295216,21.1604138 9.44574883,21.983032 L9.21798321,23.3486236 C9.07251948,24.2246212 8.99961081,25.111131 9,26 C9,26.8953847 9.0728258,27.7804297 9.21774883,28.649968 L9.44574883,30.016968 C9.58295216,30.8395862 9.19530115,31.6609921 8.47306822,32.0780114 L5.74435077,33.6535776 C5.40046982,33.851417 5.14932721,34.1778291 5.04623114,34.5609292 C4.94313508,34.9440294 4.9965408,35.3523984 5.19401753,35.6949424 L6.69795587,38.2996585 C7.11427713,39.0156351 8.03110189,39.260288 8.7470791,38.8479035 L11.4770791,37.2719035 C12.200376,36.8543519 13.1069795,36.9302031 13.7508374,37.462138 L14.8210499,38.3463136 C16.1898549,39.4774943 17.737648,40.3725891 19.3990866,40.9941596 L20.6990866,41.4791596 C21.4813437,41.7710017 22,42.5180761 22,43.353 L22,46.5 C22,47.3308348 22.6679761,48 23.493,48 L26.5007228,48.0000099 C27.328845,47.9974107 27.99906,47.3258525 28,46.5 L28,43.353 C28,42.5185702 28.5180515,41.771829 29.2996486,41.4796319 L30.599003,40.9938734 C32.261836,40.3715765 33.8093225,39.4764853 35.1790197,38.3444304 L36.2490197,37.4614304 C36.8927697,36.9301861 37.798736,36.8545694 38.5216462,37.271745 L41.25566,38.8494877 C41.598961,39.0483965 42.0072504,39.1026814 42.3905986,39.0003857 C42.7739468,38.89809 43.1009089,38.6476046 43.2989825,38.3049424 L44.8027541,35.7005546 C45.2145268,34.9820366 44.9679661,34.0658286 44.2529318,33.6520114 L41.5269318,32.0780114 C40.8046988,31.6609921 40.4170478,30.8395862 40.5542512,30.016968 L40.7821577,28.6505288 C40.9272286,27.7792134 41,26.8950523 41,26 C41,25.1046153 40.9271742,24.2195703 40.7822512,23.350032 L40.5542512,21.983032 C40.4170478,21.1604138 40.8046988,20.3390079 41.5269318,19.9219886 L44.2556492,18.3464224 C44.5995302,18.148583 44.8506728,17.8221709 44.9537689,17.4390708 C45.0568649,17.0559706 45.0034592,16.6476016 44.8059825,16.3050576 L43.3020441,13.7003415 C42.8857229,12.9843649 41.9688981,12.739712 41.2529209,13.1520965 L38.5229209,14.7280965 C37.799624,15.1456481 36.8930205,15.0697969 36.2491626,14.537862 L35.1789501,13.6536864 C33.8101451,12.5225057 32.262352,11.6274109 30.6021792,11.0063122 L29.3021792,10.5223122 C28.5192618,10.230826 28,9.48341836 28,8.648 L28,5.5 C28,4.66916515 27.3320239,4 26.507,4 L23.4992772,3.99999015 C22.671155,4.00258933 22.00094,4.67414748 22,5.5 L22,8.647 C22,9.48142977 21.4819485,10.228171 20.7003514,10.5203681 L19.400997,11.0061266 C17.738164,11.6284235 16.1906775,12.5235147 14.822142,13.6546103 C14.8121128,13.6628994 14.4553446,13.9573166 13.7518374,14.537862 Z"/>
+ <g transform="rotate(15 -59.137 82.348)">
+ <circle cx="8" cy="8" r="8" fill="#FFFFFF" transform="translate(.035 6.008)"/>
+ <path fill="#6B4FBB" d="M7.40192379,14.7679492 C2.98364579,14.7679492 -0.598076211,11.1862272 -0.598076211,6.76794919 C-0.598076211,2.34967119 2.98364579,-1.23205081 7.40192379,-1.23205081 C11.8202018,-1.23205081 15.4019238,2.34967119 15.4019238,6.76794919 C15.4019238,11.1862272 11.8202018,14.7679492 7.40192379,14.7679492 Z M7.40192379,10.7679492 C9.61106279,10.7679492 11.4019238,8.97708819 11.4019238,6.76794919 C11.4019238,4.55881019 9.61106279,2.76794919 7.40192379,2.76794919 C5.19278479,2.76794919 3.40192379,4.55881019 3.40192379,6.76794919 C3.40192379,8.97708819 5.19278479,10.7679492 7.40192379,10.7679492 Z"/>
+ </g>
+ </g>
+ <g fill-rule="nonzero" transform="rotate(15 -402.968 460.884)">
+ <path fill="#FFFFFF" d="M9.82,8.53730769 C11.1889728,7.39547918 12.7368384,6.49195101 14.4,5.86384615 L14.4,4.44230769 C14.4,1.98934615 16.376,0 18.7944,0 L21.2056,0 C22.3719078,0.00042809204 23.4902431,0.468773604 24.314422,1.30193769 C25.1386009,2.13510179 25.6010613,3.26478579 25.6,4.44230769 L25.6,5.86384615 C27.2784,6.49626923 28.824,7.40653846 30.18,8.53730769 L31.4024,7.82492308 C33.5052912,6.60101478 36.192603,7.32608729 37.4104,9.44596154 L38.616,11.5540385 C39.1987431,12.5740464 39.3561196,13.7860498 39.0534853,14.9232439 C38.750851,16.060438 38.0130146,17.0296006 37.0024,17.6173846 L35.7824,18.3289615 C35.9256,19.1980385 36,20.0897308 36,21 C36,21.9102692 35.9256,22.8019615 35.7824,23.6710385 L37.0024,24.3818077 C39.1040303,25.6085057 39.8258195,28.3210992 38.616,30.4459615 L37.4104,32.5540385 C36.8268675,33.573657 35.8659065,34.317347 34.739079,34.6213801 C33.6122515,34.9254132 32.4119396,34.7648634 31.4024,34.1750769 L30.18,33.4626923 C28.8110272,34.6045208 27.2631616,35.508049 25.6,36.1361538 L25.6,37.5576923 C25.6,40.0106538 23.624,42 21.2056,42 L18.7944,42 C17.6280922,41.9995719 16.5097569,41.5312264 15.685578,40.6980623 C14.8613991,39.8648982 14.3989387,38.7352142 14.4,37.5576923 L14.4,36.1361538 C12.7368384,35.508049 11.1889728,34.6045208 9.82,33.4626923 L8.5976,34.1750769 C6.49470875,35.3989852 3.80739703,34.6739127 2.5896,32.5540385 L1.384,30.4459615 C0.8012569,29.4259536 0.643880418,28.2139502 0.946514692,27.0767561 C1.24914897,25.939562 1.98698538,24.9703994 2.9976,24.3826154 L4.2176,23.6710385 C4.07240963,22.7882521 3.99962928,21.8948738 4,21 C4,20.0897308 4.0744,19.1980385 4.2176,18.3289615 L2.9976,17.6181923 C0.895969702,16.3914943 0.174180473,13.6789008 1.384,11.5540385 L2.5896,9.44596154 C3.17313247,8.42634297 4.13409345,7.682653 5.260921,7.37861991 C6.38774855,7.07458682 7.58806043,7.23513658 8.5976,7.82492308 L9.82,8.53730769 Z"/>
+ <path fill="#FEE1D3" d="M14.0000007,5.6038043 L14.0000013,4.44005609 C14.0029906,1.78475013 16.1390906,-0.376211234 18.7944,-0.384615385 L21.2056,-0.384615385 C23.8595941,-0.384615385 26,1.78021801 26,4.44230769 L26,5.60295806 C27.5208716,6.20655954 28.9434678,7.03621848 30.2204219,8.06411282 L31.1970056,7.49492104 C33.4941909,6.15907529 36.4301298,6.95005805 37.7609369,9.26076474 L38.9671983,11.3699991 C39.5988409,12.4761812 39.768854,13.7886936 39.4405746,15.0202941 C39.1116282,16.2543969 38.308799,17.3078735 37.2096539,17.946304 L36.2175721,18.5246428 C36.3390841,19.3401617 36.4,20.1667594 36.4,21 C36.4,21.8329668 36.339124,22.6588262 36.2175401,23.4753391 L37.2113882,24.0547082 C39.4944154,25.3886826 40.276605,28.3232105 38.9665369,30.6311583 L37.7604568,32.7400742 C37.1252608,33.8495148 36.0768547,34.6604208 34.8452776,34.9922248 C33.6111681,35.324711 32.2964469,35.1482289 31.195569,34.5042428 L30.2192355,33.9354047 C28.9426535,34.9630196 27.5206806,35.7924453 25.9999993,36.3961957 L25.9999987,37.5599439 C25.9970094,40.2152499 23.8609094,42.3762112 21.2056,42.3846154 L18.7944,42.3846154 C16.1404059,42.3846154 14,40.219782 14,37.5576923 L14,36.3970419 C12.4791284,35.7934405 11.0565322,34.9637815 9.77957815,33.9358872 L8.80299442,34.505079 C6.50580915,35.8409247 3.56987021,35.049942 2.23906313,32.7392353 L1.03280169,30.6300009 C0.401159146,29.5238188 0.231145999,28.2113064 0.559425405,26.9797059 C0.888371786,25.7456031 1.69120101,24.6921265 2.79034606,24.053696 L3.78242779,23.4753573 C3.66091587,22.6598457 3.60000002,21.8333228 3.60000019,21.0008678 C3.59964068,20.1722851 3.66061719,19.3449468 3.78254167,18.5247085 L2.78861183,17.9452918 C0.505584602,16.6113174 -0.276605002,13.6767895 1.03346313,11.3688417 L2.23954317,9.25992583 C2.87473915,8.15048519 3.92314533,7.33957919 5.15472238,7.00777521 C6.38883187,6.67528896 7.70355311,6.85177112 8.80443097,7.49575721 L9.78076186,8.06459377 C11.0573465,7.03698045 12.4793194,6.20755475 14.0000007,5.6038043 Z M11.2634746,12.0326234 C10.617233,12.5716613 9.7026973,12.6485026 8.97556903,12.2248582 L6.78774825,10.9501716 C6.60754053,10.8447551 6.39506809,10.8162338 6.19527576,10.8700606 C5.99295099,10.9245697 5.8183659,11.0596053 5.71133687,11.246543 L4.50892658,13.3490215 C4.28085652,13.7508163 4.41776119,14.2644394 4.80485394,14.4906191 L6.98565394,15.7619268 C7.70254629,16.1798426 8.08690703,16.9970357 7.95165511,17.8157512 L7.76948523,18.9184706 C7.65638664,19.6061109 7.59969735,20.3020342 7.6,21 C7.6,21.7031066 7.65662064,22.3978283 7.76925511,23.0801334 L7.95165511,24.1842488 C8.08690703,25.0029643 7.70254629,25.8201574 6.98565394,26.2380732 L4.80213007,27.5109659 C4.61772321,27.6180778 4.48116147,27.7972748 4.42448029,28.0099246 C4.36713215,28.2250767 4.39688141,28.454743 4.50573687,28.6453801 L5.70831165,30.7481858 C5.93243371,31.1373303 6.41410538,31.2670993 6.79049373,31.0482253 L8.97449373,29.7753023 C9.7016554,29.3514832 10.6163433,29.4282639 11.2626746,29.9673766 L12.1188867,30.6815536 C13.1796505,31.566598 14.3786867,32.2666727 15.6649769,32.7525215 L16.7049769,33.1442523 C17.4841581,33.4377419 18,34.1832625 18,35.0158846 L18,37.5576923 C18,38.02074 18.3597694,38.3846154 18.7944,38.3846154 L21.1992624,38.3846254 C21.6372484,38.3832375 21.9994819,38.0167881 22,37.5576923 L22,35.0158846 C22,34.18376 22.5152346,33.4385758 23.2937506,33.1447321 L24.3331012,32.7524389 C25.620867,32.2658727 26.8196661,31.5658006 27.8813806,30.679856 L28.7373806,29.9666637 C29.3836087,29.4282468 30.2976553,29.3517028 31.024431,29.7751418 L33.2122517,31.0498284 C33.3924595,31.1552449 33.6049319,31.1837662 33.8047242,31.1299394 C34.007049,31.0754303 34.1816341,30.9403947 34.2886631,30.753457 L35.4910734,28.6509785 C35.7191435,28.2491837 35.5822388,27.7355606 35.1951461,27.5093809 L33.0143461,26.2380732 C32.2974537,25.8201574 31.913093,25.0029643 32.0483449,24.1842488 L32.2306531,23.0806893 C32.3434217,22.3968737 32.4,21.7028459 32.4,21 C32.4,20.2968934 32.3433794,19.6021717 32.2307449,18.9198666 L32.0483449,17.8157512 C31.913093,16.9970357 32.2974537,16.1798426 33.0143461,15.7619268 L35.1978699,14.4890341 C35.3822768,14.3819222 35.5188385,14.2027252 35.5755197,13.9900754 C35.6328679,13.7749233 35.6031186,13.545257 35.4942631,13.3546199 L34.2916883,11.2518142 C34.0675663,10.8626697 33.5858946,10.7329007 33.2095063,10.9517747 L31.0255063,12.2246977 C30.2983446,12.6485168 29.3836567,12.5717361 28.7373254,12.0326234 L27.8811133,11.3184464 C26.8203495,10.433402 25.6213133,9.73332732 24.3362966,9.24795765 L23.2962966,8.85703457 C22.5164499,8.56389992 22,7.81804293 22,6.98492308 L22,4.44230769 C22,3.97925995 21.6402306,3.61538462 21.2056,3.61538462 L18.8007376,3.61537457 C18.3627516,3.61676247 18.0005181,3.98321188 18,4.44230769 L18,6.98411538 C18,7.81623999 17.4847654,8.56142419 16.7062494,8.85526793 L15.6668988,9.24756113 C14.379133,9.73412728 13.1803339,10.4341994 12.1197785,11.3191775 C12.1108094,11.3266617 11.8253748,11.564477 11.2634746,12.0326234 Z"/>
+ <g transform="rotate(15 -47.892 66.043)">
+ <ellipse cx="6.4" cy="6.462" fill="#FFFFFF" rx="6.4" ry="6.462" transform="translate(.028 4.853)"/>
+ <path fill="#FC6D26" d="M5.92153903,11.9125743 C2.3834711,11.9125743 -0.478460969,9.0231237 -0.478460969,5.4664205 C-0.478460969,1.9097173 2.3834711,-0.979733345 5.92153903,-0.979733345 C9.45960696,-0.979733345 12.321539,1.9097173 12.321539,5.4664205 C12.321539,9.0231237 9.45960696,11.9125743 5.92153903,11.9125743 Z M5.92153903,8.71257435 C7.6854047,8.71257435 9.12153903,7.26263103 9.12153903,5.4664205 C9.12153903,3.67020997 7.6854047,2.22026666 5.92153903,2.22026666 C4.15767337,2.22026666 2.72153903,3.67020997 2.72153903,5.4664205 C2.72153903,7.26263103 4.15767337,8.71257435 5.92153903,8.71257435 Z"/>
+ </g>
+ </g>
+ <path fill="#000000" fill-opacity=".03" d="M61.6792606,38.251778 C61.8904713,36.8653316 62,35.4454567 62,34 C62,18.536027 49.463973,6 34,6 C18.536027,6 6,18.536027 6,34 C6,49.463973 18.536027,62 34,62 C42.8132237,62 50.6754255,57.9281916 55.8080076,51.5631726 L64.2689981,50.6250607 C64.4699867,50.6027761 64.6664333,50.5501384 64.8516368,50.4689431 C65.8632575,50.0254374 66.3238058,48.8458244 65.8803001,47.8342037 L65.8803001,47.8342037 L61.6792599,38.2517794 Z"/>
+ <path fill="#FFFFFF" d="M63.6792606,34.251778 C63.8904713,32.8653316 64,31.4454567 64,30 C64,14.536027 51.463973,2 36,2 C20.536027,2 8,14.536027 8,30 C8,45.463973 20.536027,58 36,58 C44.8132237,58 52.6754255,53.9281916 57.8080076,47.5631726 L66.2689981,46.6250607 C66.4699867,46.6027761 66.6664333,46.5501384 66.8516368,46.4689431 C67.8632575,46.0254374 68.3238058,44.8458244 67.8803001,43.8342037 L67.8803001,43.8342037 L63.6792599,34.2517794 Z"/>
+ <path fill="#EEEEEE" fill-rule="nonzero" d="M69.7120015,43.0311656 C70.5990128,45.0544071 69.6779163,47.4136331 67.6546748,48.3006445 C67.2842678,48.463035 66.8913746,48.5683104 66.4893975,48.6128796 L58.8313193,49.4619687 C53.1777737,56.0908093 44.9077957,60 36,60 C19.4314575,60 6,46.5685425 6,30 C6,13.4314575 19.4314575,0 36,0 C52.5685425,0 66,13.4314575 66,30 C66,31.335699 65.9125851,32.6609639 65.739427,33.9698636 L69.7120015,43.0311656 Z M61.7020717,33.9505738 C61.8999153,32.6518726 62,31.332589 62,30 C62,15.6405965 50.3594035,4 36,4 C21.6405965,4 10,15.6405965 10,30 C10,44.3594035 21.6405965,56 36,56 C43.969518,56 51.3430155,52.3943837 56.251122,46.3077415 L56.7684631,45.6661764 L66.0485988,44.6372417 L61.8475593,35.054816 L61.6147491,34.5237842 L61.7020717,33.9505738 Z"/>
+ <g fill="#31AF64" fill-rule="nonzero" transform="translate(24 18)">
+ <path d="M12.5,26.5 C4.7680135,26.5 -1.5,20.2319865 -1.5,12.5 C-1.5,4.7680135 4.7680135,-1.5 12.5,-1.5 C20.2319865,-1.5 26.5,4.7680135 26.5,12.5 C26.5,20.2319865 20.2319865,26.5 12.5,26.5 Z M12.5,23.5 C18.5751322,23.5 23.5,18.5751322 23.5,12.5 C23.5,6.42486775 18.5751322,1.5 12.5,1.5 C6.42486775,1.5 1.5,6.42486775 1.5,12.5 C1.5,18.5751322 6.42486775,23.5 12.5,23.5 Z"/>
+ <path d="M11.18,13.81 L9.248,11.878 C8.67483243,11.3054203 7.74616757,11.3054203 7.173,11.878 C6.89709997,12.1525667 6.74198837,12.5257601 6.74198837,12.915 C6.74198837,13.3042399 6.89709997,13.6774333 7.173,13.952 L10.048,16.826 C10.0636337,16.8423622 10.0796378,16.8583663 10.096,16.874 C10.646,17.424 11.526,17.423 12.071,16.879 L17.879,11.071 C18.1403709,10.8085057 18.2866977,10.4528922 18.2857599,10.0824639 C18.2848221,9.71203558 18.1366966,9.35716757 17.874,9.096 C17.6132271,8.83256594 17.2582132,8.68392968 16.8875393,8.68299126 C16.5168653,8.68205285 16.1611034,8.82888967 15.899,9.091 L11.18,13.81 Z"/>
+ </g>
+ </g>
+</svg>
diff --git a/app/views/shared/icons/_icon_check_square_o.svg b/app/views/shared/icons/_icon_check_square_o.svg
index 3dfbfc8c0e9..05ed4d715d5 100644
--- a/app/views/shared/icons/_icon_check_square_o.svg
+++ b/app/views/shared/icons/_icon_check_square_o.svg
@@ -1 +1,2 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1472 930v318q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q63 0 117 25 15 7 18 23 3 17-9 29l-49 49q-10 10-23 10-3 0-9-2-23-6-45-6h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-254q0-13 9-22l64-64q10-10 23-10 6 0 12 3 20 8 20 29zm231-489l-814 814q-24 24-57 24t-57-24l-430-430q-24-24-24-57t24-57l110-110q24-24 57-24t57 24l263 263 647-647q24-24 57-24t57 24l110 110q24 24 24 57t-24 57z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><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"/></svg>
+
diff --git a/app/views/shared/icons/_icon_clock_o.svg b/app/views/shared/icons/_icon_clock_o.svg
index 8ddce62614c..9787ff6cb9e 100644
--- a/app/views/shared/icons/_icon_clock_o.svg
+++ b/app/views/shared/icons/_icon_clock_o.svg
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1024 544v448q0 14-9 23t-23 9h-320q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224v-352q0-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.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/></svg>
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1c.552 0 1 .448 1 1s-.448 1-1 1H8c-.276 0-.526-.112-.707-.293S7 8.277 7 8V5c0-.552.448-1 1-1s1 .448 1 1zm-1 9c-4.418 0-8-3.582-8-8s3.582-8 8-8 8 3.582 8 8-3.582 8-8 8zm0-2c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6z"/></svg>
diff --git a/app/views/shared/icons/_icon_clone.svg b/app/views/shared/icons/_icon_clone.svg
index ccc897aa98f..faba51b5797 100644
--- a/app/views/shared/icons/_icon_clone.svg
+++ b/app/views/shared/icons/_icon_clone.svg
@@ -1,3 +1,2 @@
-<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="14" viewBox="0 0 14 14">
-<path d="M13 12.75v-8.5q0-0.102-0.074-0.176t-0.176-0.074h-8.5q-0.102 0-0.176 0.074t-0.074 0.176v8.5q0 0.102 0.074 0.176t0.176 0.074h8.5q0.102 0 0.176-0.074t0.074-0.176zM14 4.25v8.5q0 0.516-0.367 0.883t-0.883 0.367h-8.5q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h8.5q0.516 0 0.883 0.367t0.367 0.883zM11 1.25v1.25h-1v-1.25q0-0.102-0.074-0.176t-0.176-0.074h-8.5q-0.102 0-0.176 0.074t-0.074 0.176v8.5q0 0.102 0.074 0.176t0.176 0.074h1.25v1h-1.25q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h8.5q0.516 0 0.883 0.367t0.367 0.883z"></path>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><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"/></svg>
+
diff --git a/app/views/shared/icons/_icon_code_fork.svg b/app/views/shared/icons/_icon_code_fork.svg
index 5a0df2eee19..968e0ad3b2b 100644
--- a/app/views/shared/icons/_icon_code_fork.svg
+++ b/app/views/shared/icons/_icon_code_fork.svg
@@ -1 +1,2 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M672 1472q0-40-28-68t-68-28-68 28-28 68 28 68 68 28 68-28 28-68zm0-1152q0-40-28-68t-68-28-68 28-28 68 28 68 68 28 68-28 28-68zm640 128q0-40-28-68t-68-28-68 28-28 68 28 68 68 28 68-28 28-68zm96 0q0 52-26 96.5t-70 69.5q-2 287-226 414-68 38-203 81-128 40-169.5 71t-41.5 100v26q44 25 70 69.5t26 96.5q0 80-56 136t-136 56-136-56-56-136q0-52 26-96.5t70-69.5v-820q-44-25-70-69.5t-26-96.5q0-80 56-136t136-56 136 56 56 136q0 52-26 96.5t-70 69.5v497q54-26 154-57 55-17 87.5-29.5t70.5-31 59-39.5 40.5-51 28-69.5 8.5-91.5q-44-25-70-69.5t-26-96.5q0-80 56-136t136-56 136 56 56 136z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><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"/></svg>
+
diff --git a/app/views/shared/icons/_icon_comment_o.svg b/app/views/shared/icons/_icon_comment_o.svg
index b99bd5f42c8..050d7356f54 100644
--- a/app/views/shared/icons/_icon_comment_o.svg
+++ b/app/views/shared/icons/_icon_comment_o.svg
@@ -1 +1,2 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M896 384q-204 0-381.5 69.5t-282 187.5-104.5 255q0 112 71.5 213.5t201.5 175.5l87 50-27 96q-24 91-70 172 152-63 275-171l43-38 57 6q69 8 130 8 204 0 381.5-69.5t282-187.5 104.5-255-104.5-255-282-187.5-381.5-69.5zm896 512q0 174-120 321.5t-326 233-450 85.5q-70 0-145-8-198 175-460 242-49 14-114 22h-5q-15 0-27-10.5t-16-27.5v-1q-3-4-.5-12t2-10 4.5-9.5l6-9 7-8.5 8-9q7-8 31-34.5t34.5-38 31-39.5 32.5-51 27-59 26-76q-157-89-247.5-220t-90.5-281q0-174 120-321.5t326-233 450-85.5 450 85.5 326 233 120 321.5z"/></svg>
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m1.707 15.707c-.63.63-1.707.184-1.707-.707v-12a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1 -3 3h-7.586zm.293-3.121 2.293-2.293a1 1 0 0 1 .707-.293h8a1 1 0 0 0 1-1v-6a1 1 0 0 0 -1-1h-10a1 1 0 0 0 -1 1z"/></svg>
+
diff --git a/app/views/shared/icons/_icon_commit.svg b/app/views/shared/icons/_icon_commit.svg
index 7e9c0ded04e..6060d2e2aa4 100644
--- a/app/views/shared/icons/_icon_commit.svg
+++ b/app/views/shared/icons/_icon_commit.svg
@@ -1 +1,2 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 18" enable-background="new 0 0 36 18"><path d="m34 7h-7.2c-.9-4-4.5-7-8.8-7s-7.9 3-8.8 7h-7.2c-1.1 0-2 .9-2 2 0 1.1.9 2 2 2h7.2c.9 4 4.5 7 8.8 7s7.9-3 8.8-7h7.2c1.1 0 2-.9 2-2 0-1.1-.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"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><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"/></svg>
+
diff --git a/app/views/shared/icons/_icon_edit.svg b/app/views/shared/icons/_icon_edit.svg
index cd4e34147e1..1c10ef138b5 100644
--- a/app/views/shared/icons/_icon_edit.svg
+++ b/app/views/shared/icons/_icon_edit.svg
@@ -1 +1,2 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M888 1184l116-116-152-152-116 116v56h96v96h56zm440-720q-16-16-33 1l-350 350q-17 17-1 33t33-1l350-350q17-17 1-33zm80 594v190q0 119-84.5 203.5t-203.5 84.5h-832q-119 0-203.5-84.5t-84.5-203.5v-832q0-119 84.5-203.5t203.5-84.5h832q63 0 117 25 15 7 18 23 3 17-9 29l-49 49q-14 14-32 8-23-6-45-6h-832q-66 0-113 47t-47 113v832q0 66 47 113t113 47h832q66 0 113-47t47-113v-126q0-13 9-22l64-64q15-15 35-7t20 29zm-96-738l288 288-672 672h-288v-288zm444 132l-92 92-288-288 92-92q28-28 68-28t68 28l152 152q28 28 28 68t-28 68z"/></svg>
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m13.436 1.413 1.415 1.414a1 1 0 0 1 0 1.414l-10.316 10.315a1 1 0 0 1 -.703.293l-2.407.008-.008-2.421a1 1 0 0 1 .293-.71l10.312-10.314a1 1 0 0 1 1.414 0zm-9.608 12.436 10.315-10.315-1.413-1.414-10.313 10.312.005 1.422zm7.486-10.313 1.414 1.414-7.778 7.778-1.407.007-.007-1.421zm1.414-1.415 1.414 1.415-.707.707-1.414-1.415z"/></svg>
+
diff --git a/app/views/shared/icons/_icon_eye.svg b/app/views/shared/icons/_icon_eye.svg
index 2e2ae67142f..9fea9841eb3 100644
--- a/app/views/shared/icons/_icon_eye.svg
+++ b/app/views/shared/icons/_icon_eye.svg
@@ -1 +1,2 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1664 960q-152-236-381-353 61 104 61 225 0 185-131.5 316.5t-316.5 131.5-316.5-131.5-131.5-316.5q0-121 61-225-229 117-381 353 133 205 333.5 326.5t434.5 121.5 434.5-121.5 333.5-326.5zm-720-384q0-20-14-34t-34-14q-125 0-214.5 89.5t-89.5 214.5q0 20 14 34t34 14 34-14 14-34q0-86 61-147t147-61q20 0 34-14t14-34zm848 384q0 34-20 69-140 230-376.5 368.5t-499.5 138.5-499.5-139-376.5-368q-20-35-20-69t20-69q140-229 376.5-368t499.5-139 499.5 139 376.5 368q20 35 20 69z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><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"/></svg>
+
diff --git a/app/views/shared/icons/_icon_eye_slash.svg b/app/views/shared/icons/_icon_eye_slash.svg
index a16c5dcb24b..45c06b3495d 100644
--- a/app/views/shared/icons/_icon_eye_slash.svg
+++ b/app/views/shared/icons/_icon_eye_slash.svg
@@ -1 +1,2 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M555 1335l78-141q-87-63-136-159t-49-203q0-121 61-225-229 117-381 353 167 258 427 375zm389-759q0-20-14-34t-34-14q-125 0-214.5 89.5t-89.5 214.5q0 20 14 34t34 14 34-14 14-34q0-86 61-147t147-61q20 0 34-14t14-34zm363-191q0 7-1 9-105 188-315 566t-316 567l-49 89q-10 16-28 16-12 0-134-70-16-10-16-28 0-12 44-87-143-65-263.5-173t-208.5-245q-20-31-20-69t20-69q153-235 380-371t496-136q89 0 180 17l54-97q10-16 28-16 5 0 18 6t31 15.5 33 18.5 31.5 18.5 19.5 11.5q16 10 16 27zm37 447q0 139-79 253.5t-209 164.5l280-502q8 45 8 84zm448 128q0 35-20 69-39 64-109 145-150 172-347.5 267t-419.5 95l74-132q212-18 392.5-137t301.5-307q-115-179-282-294l63-112q95 64 182.5 153t144.5 184q20 34 20 69z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><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"/></svg>
+
diff --git a/app/views/shared/icons/_icon_merged.svg b/app/views/shared/icons/_icon_merged.svg
index 43d591daefa..77d170b2491 100644
--- a/app/views/shared/icons/_icon_merged.svg
+++ b/app/views/shared/icons/_icon_merged.svg
@@ -1 +1,2 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m2 3c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1m.761.85c.154 2.556 1.987 4.692 4.45 5.255.328-.655 1.01-1.105 1.789-1.105 1.105 0 2 .895 2 2 0 1.105-.895 2-2 2-.89 0-1.645-.582-1.904-1.386-1.916-.376-3.548-1.5-4.596-3.044v4.493c.863.222 1.5 1.01 1.5 1.937 0 1.105-.895 2-2 2-1.105 0-2-.895-2-2 0-.74.402-1.387 1-1.732v-8.535c-.598-.346-1-.992-1-1.732 0-1.105.895-2 2-2 1.105 0 2 .895 2 2 0 .835-.512 1.551-1.239 1.85m6.239 7.15c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1m-7 4c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1" transform="translate(3)"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><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"/></svg>
+
diff --git a/app/views/shared/icons/_icon_pencil.svg b/app/views/shared/icons/_icon_pencil.svg
index a3b48404f87..bcde54bd1e1 100644
--- a/app/views/shared/icons/_icon_pencil.svg
+++ b/app/views/shared/icons/_icon_pencil.svg
@@ -1 +1,2 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M491 1536l91-91-235-235-91 91v107h128v128h107zm523-928q0-22-22-22-10 0-17 7l-542 542q-7 7-7 17 0 22 22 22 10 0 17-7l542-542q7-7 7-17zm-54-192l416 416-832 832h-416v-416zm683 96q0 53-37 90l-166 166-416-416 166-165q36-38 90-38 53 0 91 38l235 234q37 39 37 91z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><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"/></svg>
+
diff --git a/app/views/shared/icons/_icon_random.svg b/app/views/shared/icons/_icon_random.svg
index 763bd2d3dd8..74c1c903657 100644
--- a/app/views/shared/icons/_icon_random.svg
+++ b/app/views/shared/icons/_icon_random.svg
@@ -1 +1,2 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M666 481q-60 92-137 273-22-45-37-72.5t-40.5-63.5-51-56.5-63-35-81.5-14.5h-224q-14 0-23-9t-9-23v-192q0-14 9-23t23-9h224q250 0 410 225zm1126 799q0 14-9 23l-320 320q-9 9-23 9-13 0-22.5-9.5t-9.5-22.5v-192q-32 0-85 .5t-81 1-73-1-71-5-64-10.5-63-18.5-58-28.5-59-40-55-53.5-56-69.5q59-93 136-273 22 45 37 72.5t40.5 63.5 51 56.5 63 35 81.5 14.5h256v-192q0-14 9-23t23-9q12 0 24 10l319 319q9 9 9 23zm0-896q0 14-9 23l-320 320q-9 9-23 9-13 0-22.5-9.5t-9.5-22.5v-192h-256q-48 0-87 15t-69 45-51 61.5-45 77.5q-32 62-78 171-29 66-49.5 111t-54 105-64 100-74 83-90 68.5-106.5 42-128 16.5h-224q-14 0-23-9t-9-23v-192q0-14 9-23t23-9h224q48 0 87-15t69-45 51-61.5 45-77.5q32-62 78-171 29-66 49.5-111t54-105 64-100 74-83 90-68.5 106.5-42 128-16.5h256v-192q0-14 9-23t23-9q12 0 24 10l319 319q9 9 9 23z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><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"/></svg>
+
diff --git a/app/views/shared/icons/_icon_resolve_discussion.svg b/app/views/shared/icons/_icon_resolve_discussion.svg
new file mode 100644
index 00000000000..845562e9320
--- /dev/null
+++ b/app/views/shared/icons/_icon_resolve_discussion.svg
@@ -0,0 +1 @@
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><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"/></svg>
diff --git a/app/views/shared/icons/_icon_status_closed.svg b/app/views/shared/icons/_icon_status_closed.svg
index de448ee1194..dc39223e721 100644
--- a/app/views/shared/icons/_icon_status_closed.svg
+++ b/app/views/shared/icons/_icon_status_closed.svg
@@ -1 +1 @@
-<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><rect x="3.36" y="6.16" width="7.28" height="1.68" rx=".84"/></svg>
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83c.39-.39 1.024-.39 1.414 0 .39.392.39 1.025 0 1.416l-3.535 3.535c-.196.195-.452.293-.707.293-.256 0-.512-.097-.708-.292l-2.12-2.12c-.39-.392-.39-1.025 0-1.415s1.023-.39 1.413 0zM8 16c-4.418 0-8-3.582-8-8s3.582-8 8-8 8 3.582 8 8-3.582 8-8 8zm0-2c3.314 0 6-2.686 6-6s-2.686-6-6-6-6 2.686-6 6 2.686 6 6 6z"/></svg>
diff --git a/app/views/shared/icons/_icon_status_success_solid.svg b/app/views/shared/icons/_icon_status_success_solid.svg
new file mode 100644
index 00000000000..0aac6d933e1
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_success_solid.svg
@@ -0,0 +1 @@
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z 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" fill-rule="evenodd"/></svg>
diff --git a/app/views/shared/icons/_icon_tags.svg b/app/views/shared/icons/_icon_tags.svg
index fc5acc89c5e..d36ea022f92 100644
--- a/app/views/shared/icons/_icon_tags.svg
+++ b/app/views/shared/icons/_icon_tags.svg
@@ -1 +1,2 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M384 448q0-53-37.5-90.5t-90.5-37.5-90.5 37.5-37.5 90.5 37.5 90.5 90.5 37.5 90.5-37.5 37.5-90.5zm1067 576q0 53-37 90l-491 492q-39 37-91 37-53 0-90-37l-715-716q-38-37-64.5-101t-26.5-117v-416q0-52 38-90t90-38h416q53 0 117 26.5t102 64.5l715 714q37 39 37 91zm384 0q0 53-37 90l-491 492q-39 37-91 37-36 0-59-14t-53-45l470-470q37-37 37-90 0-52-37-91l-715-714q-38-38-102-64.5t-117-26.5h224q53 0 117 26.5t102 64.5l715 714q37 39 37 91z"/></svg>
+<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m7.222.222c1.657 0 3 1.343 3 3v8.327c0 .631-.298 1.226-.805 1.603l-3 2.237c-.709.529-1.682.529-2.391 0l-3-2.237c-.506-.377-.805-.972-.805-1.603v-8.327c0-1.657 1.343-3 3-3h4m-5 3v8.08c0 .158.075.306.201.401l2.5 1.864c.177.132.42.132.598 0l2.5-1.864c.127-.094.201-.243.201-.401v-8.08c0-.552-.448-1-1-1h-4c-.552 0-1 .448-1 1m2.778 7.778c-.552 0-1-.448-1-1s .448-1 1-1 1 .448 1 1-.448 1-1 1" transform="matrix(-.70711 .70711 -.70711 -.70711 17.05 9.767)"/></svg>
+
diff --git a/app/views/shared/icons/_icon_user.svg b/app/views/shared/icons/_icon_user.svg
index 9b8cd74d62b..6e7406f7eac 100644
--- a/app/views/shared/icons/_icon_user.svg
+++ b/app/views/shared/icons/_icon_user.svg
@@ -1 +1 @@
-<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1600 1405q0 120-73 189.5t-194 69.5h-874q-121 0-194-69.5t-73-189.5q0-53 3.5-103.5t14-109 26.5-108.5 43-97.5 62-81 85.5-53.5 111.5-20q9 0 42 21.5t74.5 48 108 48 133.5 21.5 133.5-21.5 108-48 74.5-48 42-21.5q61 0 111.5 20t85.5 53.5 62 81 43 97.5 26.5 108.5 14 109 3.5 103.5zm-320-893q0 159-112.5 271.5t-271.5 112.5-271.5-112.5-112.5-271.5 112.5-271.5 271.5-112.5 271.5 112.5 112.5 271.5z"/></svg>
+<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M8 7C6.343 7 5 5.657 5 4s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48S14.888 15 8 15z" fill-rule="evenodd"/></svg>
diff --git a/app/views/shared/icons/_mr_bold.svg b/app/views/shared/icons/_mr_bold.svg
index 5468545da2e..0f5be6e2bc8 100644
--- a/app/views/shared/icons/_mr_bold.svg
+++ b/app/views/shared/icons/_mr_bold.svg
@@ -1,2 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg>
-
+<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg>
diff --git a/app/views/shared/icons/_plus_square.svg b/app/views/shared/icons/_plus_square.svg
new file mode 100644
index 00000000000..7263d924f1f
--- /dev/null
+++ b/app/views/shared/icons/_plus_square.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M9 7V4c0-.552-.448-1-1-1s-1 .448-1 1v3H4c-.552 0-1 .448-1 1s.448 1 1 1h3v3c0 .552.448 1 1 1s1-.448 1-1V9h3c.552 0 1-.448 1-1s-.448-1-1-1H9zM3 0h10c1.657 0 3 1.343 3 3v10c0 1.657-1.343 3-3 3H3c-1.657 0-3-1.343-3-3V3c0-1.657 1.343-3 3-3z"/></svg>
diff --git a/app/views/shared/icons/_java_spring.svg b/app/views/shared/icons/_spring.svg
index 508349aa456..508349aa456 100644
--- a/app/views/shared/icons/_java_spring.svg
+++ b/app/views/shared/icons/_spring.svg
diff --git a/app/views/shared/icons/_thumbs_up.svg b/app/views/shared/icons/_thumbs_up.svg
new file mode 100644
index 00000000000..7267462418e
--- /dev/null
+++ b/app/views/shared/icons/_thumbs_up.svg
@@ -0,0 +1 @@
+<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/icons/_todo_done.svg b/app/views/shared/icons/_todo_done.svg
new file mode 100644
index 00000000000..156dfa11df1
--- /dev/null
+++ b/app/views/shared/icons/_todo_done.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><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 0l-2.83-2.83a1 1 0 0 1 1.415-1.413l2.123 2.12zM12 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"/></svg>
diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml
index 8a1268a1c6d..f16bc8dd430 100644
--- a/app/views/shared/issuable/_close_reopen_button.html.haml
+++ b/app/views/shared/issuable/_close_reopen_button.html.haml
@@ -4,9 +4,9 @@
- if can_update && is_current_user
= link_to "Close #{display_issuable_type}", close_issuable_url(issuable), method: button_method,
- class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
+ 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,
- class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
+ 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
- else
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 6756a7f17fd..a38cd319e3c 100644
--- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
+++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
@@ -2,7 +2,7 @@
- button_action = issuable.closed? ? 'reopen' : 'close'
- display_button_action = button_action.capitalize
- button_responsive_class = 'hidden-xs hidden-sm'
-- button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button issuable-close-button"
+- button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button js-btn-issue-action issuable-close-button"
- toggle_class = "#{button_responsive_class} btn btn-nr dropdown-toggle js-issuable-close-toggle"
- button_method = issuable_close_reopen_button_method(issuable)
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index c016aa2abcd..bb02dfa0d3a 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -29,18 +29,6 @@
= render 'shared/issuable/form/metadata', issuable: issuable, form: form
-- if issuable.can_move?(current_user)
- %hr
- .form-group
- = label_tag :move_to_project_id, 'Move', class: 'control-label'
- .col-sm-10
- .issuable-form-select-holder
- = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id), page_size: MoveToProjectFinder::PAGE_SIZE }
- &nbsp;
- %span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
- title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
- = icon('question-circle')
-
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
= render 'shared/issuable/form/merge_params', issuable: issuable
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index e8feff32d26..ad031e6af80 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -8,20 +8,19 @@
- if show_boards_content
.issue-board-dropdown-content
%p
- Create lists from the labels you use in your project. Issues with that
- label will automatically be added to the list.
+ Create lists from labels. Issues with that label appear in that list.
= dropdown_filter(filter_placeholder)
= dropdown_content
- - if @project && show_footer
+ - if current_board_parent && show_footer
= dropdown_footer do
%ul.dropdown-footer-list
- - if can?(current_user, :admin_label, @project)
+ - if can?(current_user, :admin_label, current_board_parent)
%li
%a.dropdown-toggle-page{ href: "#" }
Create new label
%li
- = link_to project_labels_path(@project), :"data-is-link" => true do
- - if show_create && @project && can?(current_user, :admin_label, @project)
+ = link_to labels_path, :"data-is-link" => true do
+ - if show_create && can?(current_user, :admin_label, current_board_parent)
Manage labels
- else
View labels
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 1ad00461d76..161b1c9fd72 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -57,7 +57,7 @@
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
No Assignee
- %li.divider
+ %li.divider.droplab-item-ignore
- if current_user
= render 'shared/issuable/user_dropdown_item',
user: current_user
@@ -76,7 +76,7 @@
%li.filter-dropdown-item{ 'data-value' => 'started' }
%button.btn.btn-link
Started
- %li.divider
+ %li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value
@@ -86,24 +86,31 @@
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
No Label
- %li.divider
+ %li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
+ #js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ %li.filter-dropdown-item
+ %button.btn.btn-link
+ %gl-emoji
+ %span.js-data-value.prepend-left-10
+ {{name}}
%button.clear-search.hidden{ type: 'button' }
= icon('times')
.filter-dropdown-container
- if type == :boards
- - if can?(current_user, :admin_list, @project)
+ - if can?(current_user, :admin_list, board.parent)
.dropdown.prepend-left-10#js-add-list
- %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path) } }
+ %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data }
Add list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- - if can?(current_user, :admin_label, @project)
+ - if can?(current_user, :admin_label, board.parent)
= render partial: "shared/issuable/label_page_create"
= dropdown_loading
#js-add-issues-btn.prepend-left-10
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index c2de6926460..0afa48b392c 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -3,7 +3,7 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sidebar')
-%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix", signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.block.issuable-sidebar-header
@@ -34,7 +34,7 @@
Milestone
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'edit-link pull-right'
+ = 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 }
@@ -60,7 +60,7 @@
Due date
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- = link_to 'Edit', '#', class: 'edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
%span.value-content
- if issuable.due_date
@@ -95,7 +95,7 @@
Labels
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
- if selected_labels.any?
- selected_labels.each do |label|
@@ -141,5 +141,22 @@
%cite{ title: project_ref }
= project_ref
= clipboard_button(text: project_ref, title: "Copy reference to clipboard", placement: "left")
+ - if current_user && issuable.can_move?(current_user)
+ .block.js-sidebar-move-issue-block
+ .sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body' }, title: 'Move issue' }
+ = custom_icon('icon_arrow_right')
+ .dropdown.sidebar-move-issue-dropdown.hide-collapsed
+ %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button',
+ data: { toggle: 'dropdown' } }
+ Move issue
+ .dropdown-menu.dropdown-menu-selectable
+ = dropdown_title('Move issue')
+ = dropdown_filter('Search project', search_id: 'sidebar-move-issue-dropdown-search')
+ = dropdown_content
+ = dropdown_loading
+ = dropdown_footer add_content_class: true do
+ %button.btn.btn-new.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ disabled: true }
+ Move
+ = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
%script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 57392cd7fbb..58782fa5f58 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -13,7 +13,7 @@
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
- if !signed_in
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
= sidebar_gutter_toggle_icon
diff --git a/app/views/shared/issuable/_user_dropdown_item.html.haml b/app/views/shared/issuable/_user_dropdown_item.html.haml
index c18e4975bb8..48d04678d47 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).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).gsub('/images/{{avatar_url}}','{{avatar_url}}').html_safe
.dropdown-user-details
%span
= user.name
diff --git a/app/views/shared/issuable/form/_issue_assignee.html.haml b/app/views/shared/issuable/form/_issue_assignee.html.haml
index 66091d95a91..9b2b6e572e7 100644
--- a/app/views/shared/issuable/form/_issue_assignee.html.haml
+++ b/app/views/shared/issuable/form/_issue_assignee.html.haml
@@ -11,7 +11,7 @@
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
- if assignees.any?
- assignees.each do |assignee|
diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
index 18011d528a0..bf8613b0f0d 100644
--- a/app/views/shared/issuable/form/_merge_request_assignee.html.haml
+++ b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
@@ -9,7 +9,7 @@
Assignee
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
- = link_to 'Edit', '#', class: 'edit-link pull-right'
+ = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
- if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 6f6a036b13f..305e2542281 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -5,7 +5,7 @@
.row
.col-sm-6
%strong= link_to truncate(milestone.title, length: 100), milestone_path
- - if milestone.is_group_milestone?
+ - if milestone.group_milestone?
%span - Group Milestone
- else
%span - Project Milestone
@@ -18,10 +18,10 @@
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
.col-sm-6= milestone_progress_bar(milestone)
- - if milestone.is_a?(GlobalMilestone) || milestone.is_group_milestone?
+ - if milestone.is_a?(GlobalMilestone) || milestone.group_milestone?
.row
.col-sm-6
- - if milestone.is_legacy_group_milestone?
+ - if milestone.legacy_group_milestone?
.expiration= render('shared/milestone_expired', milestone: milestone)
.projects
- milestone.milestones.each do |milestone|
@@ -31,8 +31,8 @@
- if @group
.col-sm-6.milestone-actions
- if can?(current_user, :admin_milestones, @group)
- - if milestone.is_group_milestone?
- = link_to edit_group_milestone_path(@group, milestone.id), class: "btn btn-xs btn-grouped" do
+ - if milestone.group_milestone?
+ = link_to edit_group_milestone_path(@group, milestone), class: "btn btn-xs btn-grouped" do
Edit
\
- if milestone.closed?
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 66ac8196f2f..f03e0ab154c 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -1,7 +1,7 @@
- affix_offset = local_assigns.fetch(:affix_offset, "50")
- project = local_assigns[:project]
-%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
@@ -21,7 +21,7 @@
.title
Start date
- if @project && can?(current_user, :admin_milestone, @project)
- = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'edit-link pull-right'
+ = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value
%span.value-content
- if milestone.start_date
@@ -51,7 +51,7 @@
.title.hide-collapsed
Due date
- if @project && can?(current_user, :admin_milestone, @project)
- = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'edit-link pull-right'
+ = link_to 'Edit', edit_project_milestone_path(@project, @milestone), class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
%span.value-content
- if milestone.due_date
@@ -88,7 +88,7 @@
.block.merge-requests
.sidebar-collapsed-icon
%strong
- = icon('exclamation', 'aria-hidden': 'true')
+ = custom_icon('mr_bold')
%span= milestone.merge_requests.count
.title.hide-collapsed
Merge requests
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index b93837e3087..fd0760d83a5 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -22,8 +22,8 @@
- if group
.pull-right
- if can?(current_user, :admin_milestones, group)
- - if milestone.is_group_milestone?
- = link_to edit_group_milestone_path(group, milestone.iid), class: "btn btn btn-grouped" do
+ - if milestone.group_milestone?
+ = link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do
Edit
- if milestone.active?
= link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-grouped btn-close"
@@ -33,7 +33,7 @@
.detail-page-description.milestone-detail
%h2.title
= markdown_field(milestone, :title)
- - if @milestone.is_group_milestone? && @milestone.description.present?
+ - if @milestone.group_milestone? && @milestone.description.present?
%div
.description
.wiki
@@ -44,7 +44,7 @@
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
-- if @milestone.is_legacy_group_milestone? || @milestone.is_dashboard_milestone?
+- if @milestone.legacy_group_milestone? || @milestone.dashboard_milestone?
.table-holder
%table.table
%thead
@@ -67,7 +67,7 @@
Open
%td
= ms.expires_at
-- elsif @milestone.is_group_milestone?
+- elsif @milestone.group_milestone?
%br
View
= link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title)
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 7174855e176..4f00a9f2759 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -28,7 +28,7 @@
commented
- if note.system
%span.system-note-message
- = note.redacted_note_html
+ = markdown_field(note, :note)
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
@@ -39,7 +39,7 @@
= render 'projects/notes/actions', note: note, note_editable: note_editable
.note-body{ class: note_editable ? 'js-task-list-container' : '' }
.note-text.md
- = note.redacted_note_html
+ = markdown_field(note, :note)
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago')
.original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index eae04c9bbb8..e3e86709b8f 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -17,9 +17,9 @@
- elsif !current_user
.disabled-comment.text-center.prepend-top-default
Please
- = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
+ = link_to "register", new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), class: 'js-register-link'
or
- = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes')
+ = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link'
to comment
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index 8939aeb6c3a..80432a73e4e 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -15,8 +15,11 @@
= link_to filter_projects_path(archived: nil), class: ("is-active" unless params[:archived].present?) do
Hide archived projects
%li
- = link_to filter_projects_path(archived: true), class: ("is-active" if params[:archived].present?) do
+ = link_to filter_projects_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do
Show archived projects
+ %li
+ = link_to filter_projects_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do
+ Show archived projects only
- if current_user
%li.divider
%li
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 914506bf0ce..0bedfea3502 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -23,6 +23,6 @@
= icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
%strong= pluralize(@private_forks_count, 'private fork')
%span &nbsp;you have no access to.
- = paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
+ = paginate_collection(projects, remote: remote)
- else
.nothing-here-block No projects found
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index f4f155c8d94..52a8fe8bb67 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -31,8 +31,7 @@
- if show_last_commit_as_description
.description.prepend-top-5
- = link_to_gfm project.commit.title, project_commit_path(project, project.commit),
- class: "commit-row-message"
+ = link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
- elsif project.description.present?
.description.prepend-top-5
= markdown_field(project, :description)
diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml
index 0fc40cf0801..87fa2007d16 100644
--- a/app/views/shared/repo/_repo.html.haml
+++ b/app/views/shared/repo/_repo.html.haml
@@ -1,2 +1,7 @@
-#repo{ data: { url: content_url, project_name: project.name, refs_url: refs_project_path(project, format: :json), project_url: project_path(project), project_id: project.id, can_commit: (!!can_push_branch?(project, @ref)).to_s } }
- %repo
+#repo{ data: { url: content_url,
+ project_name: project.name,
+ refs_url: refs_project_path(project, format: :json),
+ project_url: project_path(project),
+ project_id: project.id,
+ can_commit: (!!can_push_branch?(project, @ref)).to_s,
+ on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } }
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 17b34c5eeb3..119d189f21d 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -3,10 +3,8 @@
%span.sr-only
= visibility_level_label(@snippet.visibility_level)
= visibility_level_icon(@snippet.visibility_level, fw: false)
- %strong.item-title
- Snippet #{@snippet.to_reference}
%span.creator
- authored
+ Authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml
index 098a88c48c5..3a50324770d 100644
--- a/app/views/snippets/notes/_actions.html.haml
+++ b/app/views/snippets/notes/_actions.html.haml
@@ -1,10 +1,17 @@
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
- = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do
- = icon('spinner spin')
- %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
- %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
- %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
+ .note-actions-item
+ = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do
+ = icon('spinner spin')
+ %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
+ %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
+ %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
+
+ - if note_editable
+ .note-actions-item
+ = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do
+ %span.link-highlight
+ = custom_icon('icon_pencil')
= render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 706f13dd004..578327883e5 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,5 +1,7 @@
- @hide_top_links = true
- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout
+- add_to_breadcrumbs "Snippets", dashboard_snippets_path
+- breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
= render 'shared/snippets/header'
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 13207a8bc71..55d8d0c69d1 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -4,18 +4,45 @@ class AuthorizedProjectsWorker
# Schedules multiple jobs and waits for them to be completed.
def self.bulk_perform_and_wait(args_list)
- job_ids = bulk_perform_async(args_list)
+ # Short-circuit: it's more efficient to do small numbers of jobs inline
+ return bulk_perform_inline(args_list) if args_list.size <= 3
- Gitlab::JobWaiter.new(job_ids).wait
+ waiter = Gitlab::JobWaiter.new(args_list.size)
+
+ # Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]]
+ # into [[1, "key"], [2, "key"], [3, "key"]]
+ waiting_args_list = args_list.map { |args| [*args, waiter.key] }
+ bulk_perform_async(waiting_args_list)
+
+ waiter.wait
end
+ # Schedules multiple jobs to run in sidekiq without waiting for completion
def self.bulk_perform_async(args_list)
Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
end
- def perform(user_id)
+ # Performs multiple jobs directly. Failed jobs will be put into sidekiq so
+ # they can benefit from retries
+ def self.bulk_perform_inline(args_list)
+ failed = []
+
+ args_list.each do |args|
+ begin
+ new.perform(*args)
+ rescue
+ failed << args
+ end
+ end
+
+ bulk_perform_async(failed) if failed.present?
+ end
+
+ def perform(user_id, notify_key = nil)
user = User.find_by(id: user_id)
user&.refresh_authorized_projects
+ ensure
+ Gitlab::JobWaiter.notify(notify_key, jid) if notify_key
end
end
diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb
index f7ae996bb17..cd4af85d047 100644
--- a/app/workers/build_coverage_worker.rb
+++ b/app/workers/build_coverage_worker.rb
@@ -1,6 +1,6 @@
class BuildCoverageWorker
include Sidekiq::Worker
- include BuildQueue
+ include PipelineQueue
def perform(build_id)
Ci::Build.find_by(id: build_id)&.update_coverage
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index 466410bf08c..e2a1b3dcc41 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -1,6 +1,8 @@
class BuildFinishedWorker
include Sidekiq::Worker
- include BuildQueue
+ include PipelineQueue
+
+ enqueue_in group: :processing
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb
index 9965af935d4..dedaf2835e6 100644
--- a/app/workers/build_hooks_worker.rb
+++ b/app/workers/build_hooks_worker.rb
@@ -1,6 +1,8 @@
class BuildHooksWorker
include Sidekiq::Worker
- include BuildQueue
+ include PipelineQueue
+
+ enqueue_in group: :hooks
def perform(build_id)
Ci::Build.find_by(id: build_id)
diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb
index fa9e097e40a..e5ceb9ef715 100644
--- a/app/workers/build_queue_worker.rb
+++ b/app/workers/build_queue_worker.rb
@@ -1,6 +1,8 @@
class BuildQueueWorker
include Sidekiq::Worker
- include BuildQueue
+ include PipelineQueue
+
+ enqueue_in group: :processing
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index bf009dfab0f..20ec24bd18a 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -1,6 +1,8 @@
class BuildSuccessWorker
include Sidekiq::Worker
- include BuildQueue
+ include PipelineQueue
+
+ enqueue_in group: :processing
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
diff --git a/app/workers/concerns/build_queue.rb b/app/workers/concerns/build_queue.rb
deleted file mode 100644
index cf0ead40a8b..00000000000
--- a/app/workers/concerns/build_queue.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# Concern for setting Sidekiq settings for the various CI build workers.
-module BuildQueue
- extend ActiveSupport::Concern
-
- included do
- sidekiq_options queue: :build
- end
-end
diff --git a/app/workers/concerns/exception_backtrace.rb b/app/workers/concerns/exception_backtrace.rb
new file mode 100644
index 00000000000..ea0f1f8d19b
--- /dev/null
+++ b/app/workers/concerns/exception_backtrace.rb
@@ -0,0 +1,8 @@
+# Concern for enabling a few lines of exception backtraces in Sidekiq
+module ExceptionBacktrace
+ extend ActiveSupport::Concern
+
+ included do
+ sidekiq_options backtrace: 5
+ end
+end
diff --git a/app/workers/concerns/new_issuable.rb b/app/workers/concerns/new_issuable.rb
index 3fd472bf0c1..eb0d6c9c36c 100644
--- a/app/workers/concerns/new_issuable.rb
+++ b/app/workers/concerns/new_issuable.rb
@@ -1,20 +1,23 @@
module NewIssuable
attr_reader :issuable, :user
- def ensure_objects_found(issuable_id, user_id)
- @issuable = issuable_class.find_by(id: issuable_id)
- unless @issuable
- log_error(issuable_class, issuable_id)
- return false
- end
+ def objects_found?(issuable_id, user_id)
+ set_user(user_id)
+ set_issuable(issuable_id)
+
+ user && issuable
+ end
+ def set_user(user_id)
@user = User.find_by(id: user_id)
- unless @user
- log_error(User, user_id)
- return false
- end
- true
+ log_error(User, user_id) unless @user
+ end
+
+ def set_issuable(issuable_id)
+ @issuable = issuable_class.find_by(id: issuable_id)
+
+ log_error(issuable_class, issuable_id) unless @issuable
end
def log_error(record_class, record_id)
diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb
index ca3860e1d38..ddf45b91345 100644
--- a/app/workers/concerns/pipeline_queue.rb
+++ b/app/workers/concerns/pipeline_queue.rb
@@ -1,8 +1,18 @@
+##
# Concern for setting Sidekiq settings for the various CI pipeline workers.
+#
module PipelineQueue
extend ActiveSupport::Concern
included do
- sidekiq_options queue: :pipeline
+ sidekiq_options queue: 'pipeline_default'
+ end
+
+ class_methods do
+ def enqueue_in(group:)
+ raise ArgumentError, 'Unspecified queue group!' if group.empty?
+
+ sidekiq_options queue: "pipeline_#{group}"
+ end
end
end
diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb
index 4f47717ff69..9b5ff17aafa 100644
--- a/app/workers/create_gpg_signature_worker.rb
+++ b/app/workers/create_gpg_signature_worker.rb
@@ -4,13 +4,13 @@ class CreateGpgSignatureWorker
def perform(commit_sha, project_id)
project = Project.find_by(id: project_id)
-
return unless project
commit = project.commit(commit_sha)
return unless commit
- commit.signature
+ # This calculates and caches the signature in the database
+ Gitlab::Gpg::Commit.new(commit).signature
end
end
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
index e383202260d..98a7500bffe 100644
--- a/app/workers/expire_job_cache_worker.rb
+++ b/app/workers/expire_job_cache_worker.rb
@@ -1,6 +1,8 @@
class ExpireJobCacheWorker
include Sidekiq::Worker
- include BuildQueue
+ include PipelineQueue
+
+ enqueue_in group: :cache
def perform(job_id)
job = CommitStatus.joins(:pipeline, :project).find_by(id: job_id)
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
index 7c02d6cf892..1a0e7f92875 100644
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -2,6 +2,8 @@ class ExpirePipelineCacheWorker
include Sidekiq::Worker
include PipelineQueue
+ enqueue_in group: :cache
+
def perform(pipeline_id)
pipeline = Ci::Pipeline.find_by(id: pipeline_id)
return unless pipeline
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index c95497dfaba..ec65d3ff65e 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -5,6 +5,9 @@ class GitGarbageCollectWorker
sidekiq_options retry: false
+ # Timeout set to 24h
+ LEASE_TIMEOUT = 86400
+
GITALY_MIGRATED_TASKS = {
gc: :garbage_collect,
full_repack: :repack_full,
@@ -13,8 +16,19 @@ class GitGarbageCollectWorker
def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil)
project = Project.find(project_id)
- task = task.to_sym
+ active_uuid = get_lease_uuid(lease_key)
+
+ if active_uuid
+ return unless active_uuid == lease_uuid
+
+ renew_lease(lease_key, active_uuid)
+ else
+ lease_uuid = try_obtain_lease(lease_key)
+
+ return unless lease_uuid
+ end
+ task = task.to_sym
cmd = command(task)
repo_path = project.repository.path_to_repo
description = "'#{cmd.join(' ')}' in #{repo_path}"
@@ -33,11 +47,27 @@ class GitGarbageCollectWorker
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
ensure
- Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present?
+ cancel_lease(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present?
end
private
+ def try_obtain_lease(key)
+ ::Gitlab::ExclusiveLease.new(key, timeout: LEASE_TIMEOUT).try_obtain
+ end
+
+ def renew_lease(key, uuid)
+ ::Gitlab::ExclusiveLease.new(key, uuid: uuid, timeout: LEASE_TIMEOUT).renew
+ end
+
+ def cancel_lease(key, uuid)
+ ::Gitlab::ExclusiveLease.cancel(key, uuid)
+ end
+
+ def get_lease_uuid(key)
+ ::Gitlab::ExclusiveLease.get_uuid(key)
+ end
+
## `repository` has to be a Gitlab::Git::Repository
def gitaly_call(task, repository)
client = Gitlab::GitalyClient::RepositoryService.new(repository)
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
index 964287a1793..0ec871e00e1 100644
--- a/app/workers/gitlab_shell_worker.rb
+++ b/app/workers/gitlab_shell_worker.rb
@@ -4,6 +4,6 @@ class GitlabShellWorker
include DedicatedSidekiqQueue
def perform(action, *arg)
- gitlab_shell.send(action, *arg)
+ gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
index 07e82767b06..bd8e212e928 100644
--- a/app/workers/group_destroy_worker.rb
+++ b/app/workers/group_destroy_worker.rb
@@ -1,6 +1,7 @@
class GroupDestroyWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ include ExceptionBacktrace
def perform(group_id, user_id)
begin
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index c3b58df92c1..48e2da338f6 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -7,8 +7,6 @@ class MergeWorker
current_user = User.find(current_user_id)
merge_request = MergeRequest.find(merge_request_id)
- merge_request.update_column(:merge_jid, jid)
-
MergeRequests::MergeService.new(merge_request.target_project, current_user, params)
.execute(merge_request)
end
diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb
index bfae0c77700..f1cd1769421 100644
--- a/app/workers/namespaceless_project_destroy_worker.rb
+++ b/app/workers/namespaceless_project_destroy_worker.rb
@@ -7,6 +7,7 @@
class NamespacelessProjectDestroyWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ include ExceptionBacktrace
def self.bulk_perform_async(args_list)
Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
@@ -18,16 +19,13 @@ class NamespacelessProjectDestroyWorker
rescue ActiveRecord::RecordNotFound
return
end
- return unless project.namespace_id.nil? # Reject doing anything for projects that *do* have a namespace
+
+ return if project.namespace # Reject doing anything for projects that *do* have a namespace
project.team.truncate
unlink_fork(project) if project.forked?
- # Override Project#remove_pages for this instance so it doesn't do anything
- def project.remove_pages
- end
-
project.destroy!
end
diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb
index 19a778ad522..d9a8e892e90 100644
--- a/app/workers/new_issue_worker.rb
+++ b/app/workers/new_issue_worker.rb
@@ -4,7 +4,7 @@ class NewIssueWorker
include NewIssuable
def perform(issue_id, user_id)
- return unless ensure_objects_found(issue_id, user_id)
+ return unless objects_found?(issue_id, user_id)
EventCreateService.new.open_issue(issuable, user)
NotificationService.new.new_issue(issuable, user)
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
index 3c8a68016ff..1910c490159 100644
--- a/app/workers/new_merge_request_worker.rb
+++ b/app/workers/new_merge_request_worker.rb
@@ -4,7 +4,7 @@ class NewMergeRequestWorker
include NewIssuable
def perform(merge_request_id, user_id)
- return unless ensure_objects_found(merge_request_id, user_id)
+ return unless objects_found?(merge_request_id, user_id)
EventCreateService.new.open_mr(issuable, user)
NotificationService.new.new_merge_request(issuable, user)
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index 4eeb9666bb0..64788da7299 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -4,7 +4,7 @@ class PagesWorker
sidekiq_options queue: :pages, retry: false
def perform(action, *arg)
- send(action, *arg)
+ send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
end
def deploy(build_id)
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index 7e36eacebf8..30a75ec8435 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -2,6 +2,8 @@ class PipelineHooksWorker
include Sidekiq::Worker
include PipelineQueue
+ enqueue_in group: :hooks
+
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
.try(:execute_hooks)
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index 357e4a9a1c3..8c067d05081 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -2,6 +2,8 @@ class PipelineProcessWorker
include Sidekiq::Worker
include PipelineQueue
+ enqueue_in group: :processing
+
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
.try(:process!)
diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb
index cc0eb708cf9..cb8bb2ffe75 100644
--- a/app/workers/pipeline_success_worker.rb
+++ b/app/workers/pipeline_success_worker.rb
@@ -2,6 +2,8 @@ class PipelineSuccessWorker
include Sidekiq::Worker
include PipelineQueue
+ enqueue_in group: :processing
+
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
MergeRequests::MergeWhenPipelineSucceedsService
diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb
index 96c4152c674..5fa399dff4c 100644
--- a/app/workers/pipeline_update_worker.rb
+++ b/app/workers/pipeline_update_worker.rb
@@ -2,6 +2,8 @@ class PipelineUpdateWorker
include Sidekiq::Worker
include PipelineQueue
+ enqueue_in group: :processing
+
def perform(pipeline_id)
Ci::Pipeline.find_by(id: pipeline_id)
.try(:update_status)
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index a9188b78460..3be7e686609 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -1,6 +1,7 @@
class ProjectDestroyWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ include ExceptionBacktrace
def perform(project_id, user_id, params)
project = Project.find(project_id)
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index 6009aa1b191..f13ac9e5db2 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -1,6 +1,7 @@
class ProjectExportWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ include ExceptionBacktrace
sidekiq_options retry: 3
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index a338523dc6b..cde5b45ad41 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -5,14 +5,17 @@ class RepositoryForkWorker
include Gitlab::ShellAdapter
include DedicatedSidekiqQueue
+ sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
+
def perform(project_id, forked_from_repository_storage_path, source_path, target_path)
+ project = Project.find(project_id)
+
+ return unless start_fork(project)
+
Gitlab::Metrics.add_event(:fork_repository,
source_path: source_path,
target_path: target_path)
- project = Project.find(project_id)
- project.import_start
-
result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path,
project.repository_storage_path, target_path)
raise ForkError, "Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}" unless result
@@ -33,6 +36,13 @@ class RepositoryForkWorker
private
+ def start_fork(project)
+ return true if project.import_start
+
+ Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.")
+ false
+ end
+
def fail_fork(project, message)
Rails.logger.error(message)
project.mark_import_as_failed(message)
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 6be541abd3e..00a021abbdc 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -3,24 +3,20 @@ class RepositoryImportWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ include ExceptionBacktrace
- sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_EXPIRATION
-
- attr_accessor :project, :current_user
+ sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
def perform(project_id)
- @project = Project.find(project_id)
- @current_user = @project.creator
+ project = Project.find(project_id)
- project.import_start
+ return unless start_import(project)
Gitlab::Metrics.add_event(:import_repository,
- import_url: @project.import_url,
- path: @project.full_path)
-
- project.update_columns(import_jid: self.jid, import_error: nil)
+ import_url: project.import_url,
+ path: project.full_path)
- result = Projects::ImportService.new(project, current_user).execute
+ result = Projects::ImportService.new(project, project.creator).execute
raise ImportError, result[:message] if result[:status] == :error
project.repository.after_import
@@ -37,6 +33,13 @@ class RepositoryImportWorker
private
+ def start_import(project)
+ return true if project.import_start
+
+ Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
+ false
+ end
+
def fail_import(project, message)
project.mark_import_as_failed(message)
end
diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb
new file mode 100644
index 00000000000..c301cea5ad6
--- /dev/null
+++ b/app/workers/stage_update_worker.rb
@@ -0,0 +1,12 @@
+class StageUpdateWorker
+ include Sidekiq::Worker
+ include PipelineQueue
+
+ enqueue_in group: :processing
+
+ def perform(stage_id)
+ Ci::Stage.find_by(id: stage_id).try do |stage|
+ stage.update_status
+ end
+ end
+end
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index 8b0cfcc8af8..269776a1f62 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -53,7 +53,7 @@ class StuckCiJobsWorker
def drop_build(type, build, status, timeout)
Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout})"
Gitlab::OptimisticLocking.retry_lock(build, 3) do |b|
- b.drop
+ b.drop(:stuck_or_timeout_failure)
end
end
end
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index bfc5e667bb6..f850e459cd9 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -2,36 +2,60 @@ class StuckImportJobsWorker
include Sidekiq::Worker
include CronjobQueue
- IMPORT_EXPIRATION = 15.hours.to_i
+ IMPORT_JOBS_EXPIRATION = 15.hours.to_i
def perform
- stuck_projects.find_in_batches(batch_size: 500) do |group|
+ projects_without_jid_count = mark_projects_without_jid_as_failed!
+ projects_with_jid_count = mark_projects_with_jid_as_failed!
+
+ Gitlab::Metrics.add_event(:stuck_import_jobs,
+ projects_without_jid_count: projects_without_jid_count,
+ projects_with_jid_count: projects_with_jid_count)
+ end
+
+ private
+
+ def mark_projects_without_jid_as_failed!
+ started_projects_without_jid.each do |project|
+ project.mark_import_as_failed(error_message)
+ end.count
+ end
+
+ def mark_projects_with_jid_as_failed!
+ completed_jids_count = 0
+
+ started_projects_with_jid.find_in_batches(batch_size: 500) do |group|
jids = group.map(&:import_jid)
# Find the jobs that aren't currently running or that exceeded the threshold.
- completed_jids = Gitlab::SidekiqStatus.completed_jids(jids)
+ completed_jids = Gitlab::SidekiqStatus.completed_jids(jids).to_set
if completed_jids.any?
- completed_ids = group.select { |project| completed_jids.include?(project.import_jid) }.map(&:id)
+ completed_jids_count += completed_jids.count
+ group.each do |project|
+ project.mark_import_as_failed(error_message) if completed_jids.include?(project.import_jid)
+ end
- fail_batch!(completed_jids, completed_ids)
+ Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.to_a.join(', ')}")
end
end
- end
- private
+ completed_jids_count
+ end
- def stuck_projects
- Project.select('id, import_jid').with_import_status(:started).where.not(import_jid: nil)
+ def started_projects
+ Project.with_import_status(:started)
end
- def fail_batch!(completed_jids, completed_ids)
- Project.where(id: completed_ids).update_all(import_status: 'failed', import_error: error_message)
+ def started_projects_with_jid
+ started_projects.where.not(import_jid: nil)
+ end
- Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.join(', ')}")
+ def started_projects_without_jid
+ started_projects.where(import_jid: nil)
end
def error_message
- "Import timed out. Import took longer than #{IMPORT_EXPIRATION} seconds"
+ "Import timed out. Import took longer than #{IMPORT_JOBS_EXPIRATION} seconds"
end
end
diff --git a/bin/changelog b/bin/changelog
index 4c894f8ff5b..61d4de06e90 100755
--- a/bin/changelog
+++ b/bin/changelog
@@ -14,54 +14,107 @@ Options = Struct.new(
:dry_run,
:force,
:merge_request,
- :title
+ :title,
+ :type
)
+INVALID_TYPE = -1
class ChangelogOptionParser
- def self.parse(argv)
- options = Options.new
+ Type = Struct.new(:name, :description)
+ TYPES = [
+ Type.new('added', 'New feature'),
+ Type.new('fixed', 'Bug fix'),
+ Type.new('changed', 'Feature change'),
+ Type.new('deprecated', 'New deprecation'),
+ Type.new('removed', 'Feature removal'),
+ Type.new('security', 'Security fix'),
+ Type.new('other', 'Other')
+ ].freeze
+ TYPES_OFFSET = 1
+
+ class << self
+ def parse(argv)
+ options = Options.new
+
+ parser = OptionParser.new do |opts|
+ opts.banner = "Usage: #{__FILE__} [options] [title]\n\n"
+
+ # Note: We do not provide a shorthand for this in order to match the `git
+ # commit` interface
+ opts.on('--amend', 'Amend the previous commit') do |value|
+ options.amend = value
+ end
+
+ opts.on('-f', '--force', 'Overwrite an existing entry') do |value|
+ options.force = value
+ end
+
+ opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value|
+ options.merge_request = value
+ end
+
+ opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value|
+ options.dry_run = value
+ end
+
+ opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value|
+ options.author = git_user_name if value
+ end
+
+ opts.on('-t', '--type [string]', String, "The category of the change, valid options are: #{TYPES.map(&:name).join(', ')}") do |value|
+ options.type = parse_type(value)
+ end
+
+ opts.on('-h', '--help', 'Print help message') do
+ $stdout.puts opts
+ exit
+ end
+ end
- parser = OptionParser.new do |opts|
- opts.banner = "Usage: #{__FILE__} [options] [title]\n\n"
+ parser.parse!(argv)
- # Note: We do not provide a shorthand for this in order to match the `git
- # commit` interface
- opts.on('--amend', 'Amend the previous commit') do |value|
- options.amend = value
- end
+ # Title is everything that remains, but let's clean it up a bit
+ options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '')
- opts.on('-f', '--force', 'Overwrite an existing entry') do |value|
- options.force = value
- end
+ options
+ end
- opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value|
- options.merge_request = value
- end
+ def read_type
+ read_type_message
- opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value|
- options.dry_run = value
- end
+ type = TYPES[$stdin.getc.to_i - TYPES_OFFSET]
+ assert_valid_type!(type)
- opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value|
- options.author = git_user_name if value
- end
+ type.name
+ end
+
+ private
- opts.on('-h', '--help', 'Print help message') do
- $stdout.puts opts
- exit
+ def parse_type(name)
+ type_found = TYPES.find do |type|
+ type.name == name
end
+ type_found ? type_found.name : INVALID_TYPE
end
- parser.parse!(argv)
-
- # Title is everything that remains, but let's clean it up a bit
- options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '')
+ def read_type_message
+ $stdout.puts "\n>> Please specify the index for the category of your change:"
+ TYPES.each_with_index do |type, index|
+ $stdout.puts "#{index + TYPES_OFFSET}. #{type.description}"
+ end
+ $stdout.print "\n?> "
+ end
- options
- end
+ def assert_valid_type!(type)
+ unless type
+ $stderr.puts "Invalid category index, please select an index between 1 and #{TYPES.length}"
+ exit 1
+ end
+ end
- def self.git_user_name
- %x{git config user.name}.strip
+ def git_user_name
+ %x{git config user.name}.strip
+ end
end
end
@@ -72,8 +125,12 @@ class ChangelogEntry
@options = options
assert_feature_branch!
- assert_new_file!
assert_title!
+ assert_new_file!
+
+ # Read type from $stdin unless is already set
+ options.type ||= ChangelogOptionParser.read_type
+ assert_valid_type!
$stdout.puts "\e[32mcreate\e[0m #{file_path}"
$stdout.puts contents
@@ -90,7 +147,8 @@ class ChangelogEntry
yaml_content = YAML.dump(
'title' => title,
'merge_request' => options.merge_request,
- 'author' => options.author
+ 'author' => options.author,
+ 'type' => options.type
)
remove_trailing_whitespace(yaml_content)
end
@@ -129,6 +187,12 @@ class ChangelogEntry
" to use the title from the previous commit."
end
+ def assert_valid_type!
+ return unless options.type && options.type == INVALID_TYPE
+
+ fail_with 'Invalid category given!'
+ end
+
def title
if options.title.empty?
last_commit_subject
diff --git a/bin/rspec-stackprof b/bin/rspec-stackprof
index df79feb201d..810863ea4a0 100755
--- a/bin/rspec-stackprof
+++ b/bin/rspec-stackprof
@@ -1,5 +1,6 @@
#!/usr/bin/env ruby
+require 'bundler/setup'
require 'stackprof'
$:.unshift 'spec'
require 'rails_helper'
@@ -13,4 +14,4 @@ StackProf.run(mode: :wall, out: output_file, interval: interval) do
RSpec::Core::Runner.run(ARGV, $stderr, $stdout)
end
-system("stackprof #{output_file} --text --limit #{limit}")
+system("bundle exec stackprof #{output_file} --text --limit #{limit}")
diff --git a/changelogs/unreleased/10085-stop-encoding-user-name.yml b/changelogs/unreleased/10085-stop-encoding-user-name.yml
deleted file mode 100644
index 8fab474e047..00000000000
--- a/changelogs/unreleased/10085-stop-encoding-user-name.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Insert user name directly without encoding"
-merge_request: 10085
-author: Nathan Neulinger <nneul@neulinger.org>
diff --git a/changelogs/unreleased/12968-generalize-profile-updates.yml b/changelogs/unreleased/12968-generalize-profile-updates.yml
new file mode 100644
index 00000000000..d09793512c1
--- /dev/null
+++ b/changelogs/unreleased/12968-generalize-profile-updates.yml
@@ -0,0 +1,4 @@
+---
+title: Generalize profile updates from providers
+merge_request: 12968
+author: Alexandros Keramidas
diff --git a/changelogs/unreleased/13247-api_project_events_target_iid.yml b/changelogs/unreleased/13247-api_project_events_target_iid.yml
deleted file mode 100644
index 08a31039f77..00000000000
--- a/changelogs/unreleased/13247-api_project_events_target_iid.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Expose target_iid in Events API
-merge_request: 13247
-author: sue445
diff --git a/changelogs/unreleased/13265-project_events_noteable_iid.yml b/changelogs/unreleased/13265-project_events_noteable_iid.yml
deleted file mode 100644
index 54d538bb548..00000000000
--- a/changelogs/unreleased/13265-project_events_noteable_iid.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Expose noteable_iid in Note
-merge_request: 13265
-author: sue445
diff --git a/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml b/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml
new file mode 100644
index 00000000000..90b169390d2
--- /dev/null
+++ b/changelogs/unreleased/13325-bugfix-silence-on-disabled-notifications.yml
@@ -0,0 +1,6 @@
+---
+title: disabling notifications globally now properly turns off group/project added
+ emails
+merge_request: 13325
+author: @jneen
+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
new file mode 100644
index 00000000000..8ec78bbd41f
--- /dev/null
+++ b/changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml
@@ -0,0 +1,5 @@
+---
+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/1827-prevent-concurrent-editing-wiki.yml b/changelogs/unreleased/1827-prevent-concurrent-editing-wiki.yml
deleted file mode 100644
index c8c2bb3eb4c..00000000000
--- a/changelogs/unreleased/1827-prevent-concurrent-editing-wiki.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Alert the user if a Wiki page changed while they were editing it in order to prevent overwriting changes.
-merge_request: 9707
-author: Hiroyuki Sato
diff --git a/changelogs/unreleased/19629-remove-inactive-tokens-list.yml b/changelogs/unreleased/19629-remove-inactive-tokens-list.yml
deleted file mode 100644
index 414e3d49e29..00000000000
--- a/changelogs/unreleased/19629-remove-inactive-tokens-list.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove Inactive Personal Access Tokens list from Access Tokens page
-merge_request: 12866
-author:
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
new file mode 100644
index 00000000000..6d5baa8c10f
--- /dev/null
+++ b/changelogs/unreleased/19650-remove-admin-section-from-search-results-if-user-doesnt-have-access.yml
@@ -0,0 +1,5 @@
+---
+title: Hide admin link from default search results for non-admins
+merge_request: 14015
+author:
+type: fixed
diff --git a/changelogs/unreleased/20817-please-add-coordinator-url-to-admin-area-runner-page.yml b/changelogs/unreleased/20817-please-add-coordinator-url-to-admin-area-runner-page.yml
deleted file mode 100644
index c4c3fc7ceb2..00000000000
--- a/changelogs/unreleased/20817-please-add-coordinator-url-to-admin-area-runner-page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add coordinator url to admin area runner page
-merge_request: 11603
-author:
diff --git a/changelogs/unreleased/21949-add-type-to-changelog.yml b/changelogs/unreleased/21949-add-type-to-changelog.yml
new file mode 100644
index 00000000000..a20f6b7ad4e
--- /dev/null
+++ b/changelogs/unreleased/21949-add-type-to-changelog.yml
@@ -0,0 +1,4 @@
+---
+title: Added type to CHANGELOG entries
+merge_request:
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml b/changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml
deleted file mode 100644
index 837a34bd067..00000000000
--- a/changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Declare related resources into V4 API entities
-merge_request:
-author:
diff --git a/changelogs/unreleased/23036-replace-dashboard-event-filters-spinach.yml b/changelogs/unreleased/23036-replace-dashboard-event-filters-spinach.yml
deleted file mode 100644
index 807cd097178..00000000000
--- a/changelogs/unreleased/23036-replace-dashboard-event-filters-spinach.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replaces dashboard/event_filters.feature spinach with rspec
-merge_request: 12651
-author: Alexander Randa (@randaalex)
diff --git a/changelogs/unreleased/23036-replace-dashboard-spinach.yml b/changelogs/unreleased/23036-replace-dashboard-spinach.yml
deleted file mode 100644
index b3197c4cfa6..00000000000
--- a/changelogs/unreleased/23036-replace-dashboard-spinach.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replaces dashboard/dashboard.feature spinach with rspec
-merge_request: 12876
-author: Alexander Randa (@randaalex)
diff --git a/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml b/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml
new file mode 100644
index 00000000000..abb9e33d626
--- /dev/null
+++ b/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml
@@ -0,0 +1,5 @@
+---
+title: Removes Sortable default scope.
+merge_request: 13558
+author:
+type: fixed
diff --git a/changelogs/unreleased/26372-duplicate-issue-slash-command.yml b/changelogs/unreleased/26372-duplicate-issue-slash-command.yml
deleted file mode 100644
index 3108344e0bf..00000000000
--- a/changelogs/unreleased/26372-duplicate-issue-slash-command.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added /duplicate quick action to close a duplicate issue
-merge_request: 12845
-author: Ryan Scott
diff --git a/changelogs/unreleased/26692-predefined-variable-gitlab-user-name.yml b/changelogs/unreleased/26692-predefined-variable-gitlab-user-name.yml
new file mode 100644
index 00000000000..fa1ca3d25b2
--- /dev/null
+++ b/changelogs/unreleased/26692-predefined-variable-gitlab-user-name.yml
@@ -0,0 +1,5 @@
+---
+title: Add CI/CD job predefined variables with user name and login
+merge_request: 13824
+author:
+type: added
diff --git a/changelogs/unreleased/27616-fix-contributions-graph-utc-offset-mysql.yml b/changelogs/unreleased/27616-fix-contributions-graph-utc-offset-mysql.yml
deleted file mode 100644
index 1b3c3b8538d..00000000000
--- a/changelogs/unreleased/27616-fix-contributions-graph-utc-offset-mysql.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix timezone inconsistencies in user contribution graph
-merge_request: 13208
-author:
diff --git a/changelogs/unreleased/28202_decrease_abc_threshold_step2.yml b/changelogs/unreleased/28202_decrease_abc_threshold_step2.yml
deleted file mode 100644
index b8f30b52b18..00000000000
--- a/changelogs/unreleased/28202_decrease_abc_threshold_step2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Decrease ABC threshold to 56.96
-merge_request: 11227
-author: Maxim Rydkin
diff --git a/changelogs/unreleased/28202_decrease_abc_threshold_step3.yml b/changelogs/unreleased/28202_decrease_abc_threshold_step3.yml
new file mode 100644
index 00000000000..ed38fd37103
--- /dev/null
+++ b/changelogs/unreleased/28202_decrease_abc_threshold_step3.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..283e06d4b7f
--- /dev/null
+++ b/changelogs/unreleased/28283-uuid-storage.yml
@@ -0,0 +1,4 @@
+---
+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
new file mode 100644
index 00000000000..129cf505a3f
--- /dev/null
+++ b/changelogs/unreleased/28453-add-time-estimate-time-spent-to-api-issue-output.yml
@@ -0,0 +1,4 @@
+---
+title: Add time stats to Issue and Merge Request API
+merge_request: 13335
+author: @travismiller
diff --git a/changelogs/unreleased/28472-ignore-auto-generated-mails.yml b/changelogs/unreleased/28472-ignore-auto-generated-mails.yml
deleted file mode 100644
index af63b43e62e..00000000000
--- a/changelogs/unreleased/28472-ignore-auto-generated-mails.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't send rejection mails for all auto-generated mails
-merge_request: 13254
-author:
diff --git a/changelogs/unreleased/28938-password-change-workflow-for-admins.yml b/changelogs/unreleased/28938-password-change-workflow-for-admins.yml
new file mode 100644
index 00000000000..0781e1a2fce
--- /dev/null
+++ b/changelogs/unreleased/28938-password-change-workflow-for-admins.yml
@@ -0,0 +1,5 @@
+---
+title: Changes the password change workflow for admins.
+merge_request: 13901
+author:
+type: fixed
diff --git a/changelogs/unreleased/29289-project-destroy-clean-up-after-failure.yml b/changelogs/unreleased/29289-project-destroy-clean-up-after-failure.yml
deleted file mode 100644
index 488b37ac37f..00000000000
--- a/changelogs/unreleased/29289-project-destroy-clean-up-after-failure.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Handle errors while a project is being deleted asynchronously.
-merge_request: 11088
-author:
diff --git a/changelogs/unreleased/29385-add_shrug_command.yml b/changelogs/unreleased/29385-add_shrug_command.yml
deleted file mode 100644
index 14b8f486d5c..00000000000
--- a/changelogs/unreleased/29385-add_shrug_command.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add /shrug and /tableflip commands
-merge_request: 10068
-author: Alex Ives
diff --git a/changelogs/unreleased/29811-fix-line-number-alignment.yml b/changelogs/unreleased/29811-fix-line-number-alignment.yml
new file mode 100644
index 00000000000..94b3328a7f2
--- /dev/null
+++ b/changelogs/unreleased/29811-fix-line-number-alignment.yml
@@ -0,0 +1,4 @@
+---
+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/29901-refactor-initialization-dropzone_input-js.yml b/changelogs/unreleased/29901-refactor-initialization-dropzone_input-js.yml
deleted file mode 100644
index 8850422fc88..00000000000
--- a/changelogs/unreleased/29901-refactor-initialization-dropzone_input-js.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: refactor initializations in dropzone_input.js
-merge_request: 12768
-author: Brandon Everett
diff --git a/changelogs/unreleased/30162-retire-koding-integration.yml b/changelogs/unreleased/30162-retire-koding-integration.yml
new file mode 100644
index 00000000000..63c2b9eb161
--- /dev/null
+++ b/changelogs/unreleased/30162-retire-koding-integration.yml
@@ -0,0 +1,4 @@
+---
+title: Deprecation of Koding integration, removal of setting in Admin Panel
+merge_request: 13992
+author: @mydigitalself
diff --git a/changelogs/unreleased/30634-protected-pipeline.yml b/changelogs/unreleased/30634-protected-pipeline.yml
deleted file mode 100644
index e46538e5b46..00000000000
--- a/changelogs/unreleased/30634-protected-pipeline.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Disallow running the pipeline if ref is protected and user cannot merge the
- branch or create the tag
-merge_request: 11910
-author:
diff --git a/changelogs/unreleased/31129-jira-project-key-elim.yml b/changelogs/unreleased/31129-jira-project-key-elim.yml
deleted file mode 100644
index bfa0e99f250..00000000000
--- a/changelogs/unreleased/31129-jira-project-key-elim.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove project_key from the Jira configuration
-merge_request: 12050
-author:
diff --git a/changelogs/unreleased/31207-clean-locked-merge-requests.yml b/changelogs/unreleased/31207-clean-locked-merge-requests.yml
deleted file mode 100644
index 1f52987baef..00000000000
--- a/changelogs/unreleased/31207-clean-locked-merge-requests.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Unlock stuck merge request and set the proper state
-merge_request: 13207
-author:
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
new file mode 100644
index 00000000000..4d21717e161
--- /dev/null
+++ b/changelogs/unreleased/31273-creating-an-project-within-an-internal-sub-group-gives-the-option-to-set-it-a-public.yml
@@ -0,0 +1,6 @@
+---
+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/31362_decrease_cyclomatic_complexity_threshold_step3.yml b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml
new file mode 100644
index 00000000000..4a8d8097169
--- /dev/null
+++ b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml
@@ -0,0 +1,5 @@
+---
+title: Decrease Cyclomatic Complexity threshold to 14
+merge_request: 13972
+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
new file mode 100644
index 00000000000..06e8180db64
--- /dev/null
+++ b/changelogs/unreleased/31409-fix-group-and-project-search-for-anonymous-users.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..271a945a998
--- /dev/null
+++ b/changelogs/unreleased/31470-fix-api-files-raw.yml
@@ -0,0 +1,5 @@
+---
+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/31533-usage-data-projects-stats.yml b/changelogs/unreleased/31533-usage-data-projects-stats.yml
deleted file mode 100644
index 11bb6118337..00000000000
--- a/changelogs/unreleased/31533-usage-data-projects-stats.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add Slack and JIRA services counts to Usage Data
-merge_request:
-author:
diff --git a/changelogs/unreleased/31571-don-t-let-webhooks-jobs-go-to-the-dead-jobs-queue.yml b/changelogs/unreleased/31571-don-t-let-webhooks-jobs-go-to-the-dead-jobs-queue.yml
deleted file mode 100644
index 69900f0b314..00000000000
--- a/changelogs/unreleased/31571-don-t-let-webhooks-jobs-go-to-the-dead-jobs-queue.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent web hook and project service background jobs from going to the dead
- jobs queue
-merge_request:
-author:
diff --git a/changelogs/unreleased/32483-jira-error.yml b/changelogs/unreleased/32483-jira-error.yml
deleted file mode 100644
index 1c530ca5e0f..00000000000
--- a/changelogs/unreleased/32483-jira-error.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Display specific error message when JIRA test fails
-merge_request:
-author:
diff --git a/changelogs/unreleased/32665-refactor-project-visibility-settings.yml b/changelogs/unreleased/32665-refactor-project-visibility-settings.yml
new file mode 100644
index 00000000000..fde70c47ca6
--- /dev/null
+++ b/changelogs/unreleased/32665-refactor-project-visibility-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Redesign project feature permissions settings
+merge_request: 14062
+author:
+type: changed
diff --git a/changelogs/unreleased/32844-issuables-performance.yml b/changelogs/unreleased/32844-issuables-performance.yml
deleted file mode 100644
index e9b21c1aa45..00000000000
--- a/changelogs/unreleased/32844-issuables-performance.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Move some code from services to workers in order to improve performance
-merge_request: 13326
-author:
diff --git a/changelogs/unreleased/33095-mr-widget-ui.yml b/changelogs/unreleased/33095-mr-widget-ui.yml
deleted file mode 100644
index 9ce3086df27..00000000000
--- a/changelogs/unreleased/33095-mr-widget-ui.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: clean up merge request widget UI
-merge_request:
-author:
diff --git a/changelogs/unreleased/33097-issue-tracker.yml b/changelogs/unreleased/33097-issue-tracker.yml
deleted file mode 100644
index 0b13f7165db..00000000000
--- a/changelogs/unreleased/33097-issue-tracker.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Associate Issues tab only with internal issues tracker
-merge_request:
-author:
diff --git a/changelogs/unreleased/33601-add-csrf-token-verification-to-api.yml b/changelogs/unreleased/33601-add-csrf-token-verification-to-api.yml
deleted file mode 100644
index 88cfb99a73e..00000000000
--- a/changelogs/unreleased/33601-add-csrf-token-verification-to-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add CSRF token verification to API
-merge_request: 12154
-author: Vitaliy @blackst0ne Klachkov
diff --git a/changelogs/unreleased/33620-remove-events-from-notification_settings.yml b/changelogs/unreleased/33620-remove-events-from-notification_settings.yml
deleted file mode 100644
index f5f3ef3fb82..00000000000
--- a/changelogs/unreleased/33620-remove-events-from-notification_settings.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove events column from notification settings table
-merge_request:
-author:
diff --git a/changelogs/unreleased/33741-clarify-k8s-service-keys.yml b/changelogs/unreleased/33741-clarify-k8s-service-keys.yml
deleted file mode 100644
index 91142a0d580..00000000000
--- a/changelogs/unreleased/33741-clarify-k8s-service-keys.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Clarifies and rearranges the input variables on the kubernetes integration
- page and adjusts the docs slightly to meet the same order
-merge_request: !12188
-author:
diff --git a/changelogs/unreleased/33770-respect-blockquote-line-breaks.yml b/changelogs/unreleased/33770-respect-blockquote-line-breaks.yml
deleted file mode 100644
index 3a45ad88270..00000000000
--- a/changelogs/unreleased/33770-respect-blockquote-line-breaks.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Respect blockquote line breaks in markdown
-merge_request:
-author:
diff --git a/changelogs/unreleased/33874_confi.yml b/changelogs/unreleased/33874_confi.yml
deleted file mode 100644
index 940753d9aaa..00000000000
--- a/changelogs/unreleased/33874_confi.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update confidential issue UI - add confidential visibility and settings to
- sidebar
-merge_request:
-author:
diff --git a/changelogs/unreleased/34027-add-icons-to-sidebar.yml b/changelogs/unreleased/34027-add-icons-to-sidebar.yml
deleted file mode 100644
index f5b50ca1dee..00000000000
--- a/changelogs/unreleased/34027-add-icons-to-sidebar.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add icons to contextual sidebars
-merge_request:
-author:
diff --git a/changelogs/unreleased/34028-collapse-sidebar.yml b/changelogs/unreleased/34028-collapse-sidebar.yml
deleted file mode 100644
index 468212240ac..00000000000
--- a/changelogs/unreleased/34028-collapse-sidebar.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make contextual sidebar collapsible
-merge_request:
-author:
diff --git a/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml b/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml
new file mode 100644
index 00000000000..278ef2a8acb
--- /dev/null
+++ b/changelogs/unreleased/34049-public-commits-should-not-require-authentication.yml
@@ -0,0 +1,4 @@
+---
+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/34075-pipelines-count-mt.yml b/changelogs/unreleased/34075-pipelines-count-mt.yml
deleted file mode 100644
index 3846e7b06a4..00000000000
--- a/changelogs/unreleased/34075-pipelines-count-mt.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Update Pipeline's badge count in Merge Request and Commits view to match real-time
- content
-merge_request:
-author:
diff --git a/changelogs/unreleased/34110-memory-usage-notice-doesn-t-link-anywhere.yml b/changelogs/unreleased/34110-memory-usage-notice-doesn-t-link-anywhere.yml
deleted file mode 100644
index 1911705dd2b..00000000000
--- a/changelogs/unreleased/34110-memory-usage-notice-doesn-t-link-anywhere.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Added link to the MR widget that directs to the monitoring dashboard
-merge_request:
-author:
diff --git a/changelogs/unreleased/34261-move-move-to-sidebar.yml b/changelogs/unreleased/34261-move-move-to-sidebar.yml
new file mode 100644
index 00000000000..59fa1d4c221
--- /dev/null
+++ b/changelogs/unreleased/34261-move-move-to-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Move "Move issue" controls to right-sidebar
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/34361-lazy-load-images-on-the-frontend.yml b/changelogs/unreleased/34361-lazy-load-images-on-the-frontend.yml
deleted file mode 100644
index d188a558d38..00000000000
--- a/changelogs/unreleased/34361-lazy-load-images-on-the-frontend.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Lazy load images for better Frontend performance
-merge_request: 12503
-author:
diff --git a/changelogs/unreleased/34371-pipeline-schedule-vue-files.yml b/changelogs/unreleased/34371-pipeline-schedule-vue-files.yml
new file mode 100644
index 00000000000..7de30d82601
--- /dev/null
+++ b/changelogs/unreleased/34371-pipeline-schedule-vue-files.yml
@@ -0,0 +1,6 @@
+---
+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
new file mode 100644
index 00000000000..d33b55ef681
--- /dev/null
+++ b/changelogs/unreleased/34413-move-convdev-index-location-to-after-cohorts.yml
@@ -0,0 +1,4 @@
+---
+title: Move ConvDev Index location to after Cohorts.
+merge_request: !13398
+author:
diff --git a/changelogs/unreleased/34492-firefox-job.yml b/changelogs/unreleased/34492-firefox-job.yml
deleted file mode 100644
index 881b8f649ea..00000000000
--- a/changelogs/unreleased/34492-firefox-job.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use jQuery to control scroll behavior in job log for cross browser consistency
-merge_request:
-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
new file mode 100644
index 00000000000..a61d703bacd
--- /dev/null
+++ b/changelogs/unreleased/34509-improves-markdown-rendering-performance-for-commits-list.yml
@@ -0,0 +1,5 @@
+---
+title: Improves markdown rendering performance for commit lists.
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/34519-extend-api-group-secret-variable.yml b/changelogs/unreleased/34519-extend-api-group-secret-variable.yml
deleted file mode 100644
index e0b625c392f..00000000000
--- a/changelogs/unreleased/34519-extend-api-group-secret-variable.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Extend API for Group Secret Variable
-merge_request: 12936
-author:
diff --git a/changelogs/unreleased/34534-update-vue-resource.yml b/changelogs/unreleased/34534-update-vue-resource.yml
deleted file mode 100644
index 2d0af0c9bfe..00000000000
--- a/changelogs/unreleased/34534-update-vue-resource.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Updates vue resource and code according to breaking changes
-merge_request:
-author:
diff --git a/changelogs/unreleased/34549-extract-devise-mappings-into-helper.yml b/changelogs/unreleased/34549-extract-devise-mappings-into-helper.yml
deleted file mode 100644
index e843bbac239..00000000000
--- a/changelogs/unreleased/34549-extract-devise-mappings-into-helper.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Extract "@request.env[devise.mapping] = Devise.mappings[:user]" to a test helper
-merge_request: 12742
-author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/34563-usage-ping-github.yml b/changelogs/unreleased/34563-usage-ping-github.yml
deleted file mode 100644
index 3ab982beea3..00000000000
--- a/changelogs/unreleased/34563-usage-ping-github.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add GitHub imported projects count to usage data
-merge_request:
-author:
diff --git a/changelogs/unreleased/34643-fix-project-path-slugify.yml b/changelogs/unreleased/34643-fix-project-path-slugify.yml
new file mode 100644
index 00000000000..f7018a1aca5
--- /dev/null
+++ b/changelogs/unreleased/34643-fix-project-path-slugify.yml
@@ -0,0 +1,4 @@
+---
+title: Fix CI_PROJECT_PATH_SLUG slugify
+merge_request: 13350
+author: Ivan Chernov
diff --git a/changelogs/unreleased/34764-rename-to-overview.yml b/changelogs/unreleased/34764-rename-to-overview.yml
deleted file mode 100644
index 5b9643285b7..00000000000
--- a/changelogs/unreleased/34764-rename-to-overview.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Rename about to overview for group and project page
-merge_request:
-author:
diff --git a/changelogs/unreleased/34810-vue-pagination.yml b/changelogs/unreleased/34810-vue-pagination.yml
deleted file mode 100644
index 5cd03518a98..00000000000
--- a/changelogs/unreleased/34810-vue-pagination.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent disabled pagination button to be clicked
-merge_request:
-author:
diff --git a/changelogs/unreleased/34831-remove-coffee-rails-gem.yml b/changelogs/unreleased/34831-remove-coffee-rails-gem.yml
deleted file mode 100644
index b555f112b8d..00000000000
--- a/changelogs/unreleased/34831-remove-coffee-rails-gem.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove coffee-rails gem
-merge_request:
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/34858-bump-scss-lint-to-0-54-0.yml b/changelogs/unreleased/34858-bump-scss-lint-to-0-54-0.yml
deleted file mode 100644
index e6cd834aed2..00000000000
--- a/changelogs/unreleased/34858-bump-scss-lint-to-0-54-0.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Bump scss-lint to 0.54.0
-merge_request: 12733
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/34867-remove-net-ssh-gem.yml b/changelogs/unreleased/34867-remove-net-ssh-gem.yml
deleted file mode 100644
index f5648d62467..00000000000
--- a/changelogs/unreleased/34867-remove-net-ssh-gem.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove net-ssh gem
-merge_request:
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/34869-bump-rubocop-to-0-49-1-and-rubocop-rspec-to-1-15-1.yml b/changelogs/unreleased/34869-bump-rubocop-to-0-49-1-and-rubocop-rspec-to-1-15-1.yml
deleted file mode 100644
index 0eb2d069719..00000000000
--- a/changelogs/unreleased/34869-bump-rubocop-to-0-49-1-and-rubocop-rspec-to-1-15-1.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Bump rubocop to 0.49.1 and rubocop-rspec to 1.15.1
-merge_request:
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/34921-global-dropdown-ui-improvement.yml b/changelogs/unreleased/34921-global-dropdown-ui-improvement.yml
deleted file mode 100644
index 6a17353ba3f..00000000000
--- a/changelogs/unreleased/34921-global-dropdown-ui-improvement.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve CSS for global nav dropdown UI
-merge_request: 12772
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/34927-protect-manual-actions-on-tags.yml b/changelogs/unreleased/34927-protect-manual-actions-on-tags.yml
deleted file mode 100644
index d996ae2826a..00000000000
--- a/changelogs/unreleased/34927-protect-manual-actions-on-tags.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Protect manual actions against protected tag too
-merge_request: 12908
-author:
diff --git a/changelogs/unreleased/34945-readme-div-id.yml b/changelogs/unreleased/34945-readme-div-id.yml
new file mode 100644
index 00000000000..c7d26b746b4
--- /dev/null
+++ b/changelogs/unreleased/34945-readme-div-id.yml
@@ -0,0 +1,5 @@
+---
+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/34978-remove-public-ci-favicon-ico.yml b/changelogs/unreleased/34978-remove-public-ci-favicon-ico.yml
deleted file mode 100644
index 25cc8b5e45f..00000000000
--- a/changelogs/unreleased/34978-remove-public-ci-favicon-ico.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove public/ci/favicon.ico
-merge_request: 12803
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/34990-top-buttons-misaligned.yml b/changelogs/unreleased/34990-top-buttons-misaligned.yml
new file mode 100644
index 00000000000..db60f83ed71
--- /dev/null
+++ b/changelogs/unreleased/34990-top-buttons-misaligned.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..c5bed723f55
--- /dev/null
+++ b/changelogs/unreleased/35010-projects-nav-dropdown.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..6cd7f4e9cc6
--- /dev/null
+++ b/changelogs/unreleased/35010-remove-goto-project-from-breadcrumb.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..74aa337a18c
--- /dev/null
+++ b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml
@@ -0,0 +1,5 @@
+---
+title: Add option in preferences to change navigation theme color
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/35044-projects-logo-are-not-centered-vertically-on-projects-page.yml b/changelogs/unreleased/35044-projects-logo-are-not-centered-vertically-on-projects-page.yml
deleted file mode 100644
index 9de4dbefd35..00000000000
--- a/changelogs/unreleased/35044-projects-logo-are-not-centered-vertically-on-projects-page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix project logos that are not centered vertically on list pages
-merge_request: 13124
-author: Florian Lemaitre
diff --git a/changelogs/unreleased/35048-empty-badges.yml b/changelogs/unreleased/35048-empty-badges.yml
new file mode 100644
index 00000000000..816fe82887c
--- /dev/null
+++ b/changelogs/unreleased/35048-empty-badges.yml
@@ -0,0 +1,5 @@
+---
+title: Prevents rendering empty badges when request fails
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/35136-barchart-not-display-label-at-0-hour.yml b/changelogs/unreleased/35136-barchart-not-display-label-at-0-hour.yml
deleted file mode 100644
index ea8f31cca9d..00000000000
--- a/changelogs/unreleased/35136-barchart-not-display-label-at-0-hour.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix bar chart does not display label at 0 hour
-merge_request: 35136
-author: Jason Dai
diff --git a/changelogs/unreleased/35155-upgrade-fog-core-to-1-44-3-and-its-providers-to-the-latest.yml b/changelogs/unreleased/35155-upgrade-fog-core-to-1-44-3-and-its-providers-to-the-latest.yml
deleted file mode 100644
index 9d9558347ba..00000000000
--- a/changelogs/unreleased/35155-upgrade-fog-core-to-1-44-3-and-its-providers-to-the-latest.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Bump fog-core to 1.44.3 and fog providers' plugins to latest
-merge_request: 12897
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/35161_first_time_contributor_badge.yml b/changelogs/unreleased/35161_first_time_contributor_badge.yml
new file mode 100644
index 00000000000..f3ab2d9db31
--- /dev/null
+++ b/changelogs/unreleased/35161_first_time_contributor_badge.yml
@@ -0,0 +1,4 @@
+---
+title: "First-time contributor badge"
+merge_request: 13143
+author: Micaël Bergeron <micaelbergeron@gmail.com>
diff --git a/changelogs/unreleased/35163-url-in-commit-message-can-be-broken-in-blame.yml b/changelogs/unreleased/35163-url-in-commit-message-can-be-broken-in-blame.yml
deleted file mode 100644
index 4fd60a79782..00000000000
--- a/changelogs/unreleased/35163-url-in-commit-message-can-be-broken-in-blame.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Use only CSS to truncate commit message in blame
-merge_request: 12900
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/35164-cycle-analytics-firefox.yml b/changelogs/unreleased/35164-cycle-analytics-firefox.yml
deleted file mode 100644
index 0b7115136ca..00000000000
--- a/changelogs/unreleased/35164-cycle-analytics-firefox.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: allow closing Cycle Analytics intro box in firefox
-merge_request:
-author:
diff --git a/changelogs/unreleased/35181-cannot-create-label-from-board-page.yml b/changelogs/unreleased/35181-cannot-create-label-from-board-page.yml
deleted file mode 100644
index 4afe603720d..00000000000
--- a/changelogs/unreleased/35181-cannot-create-label-from-board-page.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix label creation from new list for subgroup projects
-merge_request:
-author:
diff --git a/changelogs/unreleased/35191-prioritized-labels-for-non-member.yml b/changelogs/unreleased/35191-prioritized-labels-for-non-member.yml
deleted file mode 100644
index fbe55d4c2b0..00000000000
--- a/changelogs/unreleased/35191-prioritized-labels-for-non-member.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove help message about prioritized labels for non-members
-merge_request: 12912
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/35204-doc-api-ci-lint-typo.yml b/changelogs/unreleased/35204-doc-api-ci-lint-typo.yml
deleted file mode 100644
index 45b6c57579b..00000000000
--- a/changelogs/unreleased/35204-doc-api-ci-lint-typo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add link to doc/api/ci/lint.md
-merge_request: 12914
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/35225-transient-poll.yml b/changelogs/unreleased/35225-transient-poll.yml
deleted file mode 100644
index 59e2e738c7b..00000000000
--- a/changelogs/unreleased/35225-transient-poll.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: fix transient js error in rspec tests
-merge_request:
-author:
diff --git a/changelogs/unreleased/35232-next-unresolved.yml b/changelogs/unreleased/35232-next-unresolved.yml
deleted file mode 100644
index 45f3fb429a8..00000000000
--- a/changelogs/unreleased/35232-next-unresolved.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: fix jump to next discussion button
-merge_request:
-author:
diff --git a/changelogs/unreleased/35253-desc-protected-branches-for-non-member.yml b/changelogs/unreleased/35253-desc-protected-branches-for-non-member.yml
deleted file mode 100644
index 9b2a66da1c3..00000000000
--- a/changelogs/unreleased/35253-desc-protected-branches-for-non-member.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Hide description about protected branches to non-member
-merge_request: 12945
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/35343-inherit-milestones-and-labels.yml b/changelogs/unreleased/35343-inherit-milestones-and-labels.yml
new file mode 100644
index 00000000000..ce737a67356
--- /dev/null
+++ b/changelogs/unreleased/35343-inherit-milestones-and-labels.yml
@@ -0,0 +1,5 @@
+---
+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/35391-fix-star-i18n-in-js.yml b/changelogs/unreleased/35391-fix-star-i18n-in-js.yml
deleted file mode 100644
index a6fd4dc89fd..00000000000
--- a/changelogs/unreleased/35391-fix-star-i18n-in-js.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix translations for Star/Unstar in JS file
-merge_request:
-author:
diff --git a/changelogs/unreleased/35408-group-auto-avatars.yml b/changelogs/unreleased/35408-group-auto-avatars.yml
deleted file mode 100644
index 77b644a7f94..00000000000
--- a/changelogs/unreleased/35408-group-auto-avatars.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Show auto-generated avatars for Groups without avatars
-merge_request: 13188
-author:
diff --git a/changelogs/unreleased/35441-fix-division-by-zero.yml b/changelogs/unreleased/35441-fix-division-by-zero.yml
new file mode 100644
index 00000000000..335b2d40494
--- /dev/null
+++ b/changelogs/unreleased/35441-fix-division-by-zero.yml
@@ -0,0 +1,5 @@
+---
+title: Fix division by zero error in blame age mapping
+merge_request: 13803
+author: Jeff Stubler
+type: fixed
diff --git a/changelogs/unreleased/35483-improve-mobile-sidebar.yml b/changelogs/unreleased/35483-improve-mobile-sidebar.yml
deleted file mode 100644
index eb3dab1da9e..00000000000
--- a/changelogs/unreleased/35483-improve-mobile-sidebar.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve mobile sidebar
-merge_request:
-author:
diff --git a/changelogs/unreleased/35659-rename-pipeline.yml b/changelogs/unreleased/35659-rename-pipeline.yml
deleted file mode 100644
index 0fe211868e4..00000000000
--- a/changelogs/unreleased/35659-rename-pipeline.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Rename Pipelines tab to CI / CD in new navigation
-merge_request:
-author:
diff --git a/changelogs/unreleased/35686-unescape-wiki-title.yml b/changelogs/unreleased/35686-unescape-wiki-title.yml
new file mode 100644
index 00000000000..4b2b7078163
--- /dev/null
+++ b/changelogs/unreleased/35686-unescape-wiki-title.yml
@@ -0,0 +1,5 @@
+---
+title: Unescape HTML characters in Wiki title
+merge_request: 13942
+author: Jacopo Beschi @jacopo-beschi
+type: fixed
diff --git a/changelogs/unreleased/35695-comment-appears-in-a-wrong-place-after-changing-diff-view-to-inline.yml b/changelogs/unreleased/35695-comment-appears-in-a-wrong-place-after-changing-diff-view-to-inline.yml
deleted file mode 100644
index 1c9ad20bc95..00000000000
--- a/changelogs/unreleased/35695-comment-appears-in-a-wrong-place-after-changing-diff-view-to-inline.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix display of new diff comments after changing b between diff views
-merge_request:
-author:
diff --git a/changelogs/unreleased/35697-allow-logged-in-user-to-read-user-list.yml b/changelogs/unreleased/35697-allow-logged-in-user-to-read-user-list.yml
deleted file mode 100644
index 54b2e71bef9..00000000000
--- a/changelogs/unreleased/35697-allow-logged-in-user-to-read-user-list.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow any logged in users to read_users_list even if it's restricted
-merge_request: 13201
-author:
diff --git a/changelogs/unreleased/35721-auth-style-confirmation.yml b/changelogs/unreleased/35721-auth-style-confirmation.yml
new file mode 100644
index 00000000000..9963f76e845
--- /dev/null
+++ b/changelogs/unreleased/35721-auth-style-confirmation.yml
@@ -0,0 +1,5 @@
+---
+title: restyling of OAuth authorization confirmation
+merge_request:
+author: Jacopo Beschi @jacopo-beschi
+type: changed
diff --git a/changelogs/unreleased/35761-convdev-perc.yml b/changelogs/unreleased/35761-convdev-perc.yml
deleted file mode 100644
index 319c4d18219..00000000000
--- a/changelogs/unreleased/35761-convdev-perc.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Store & use ConvDev percentages returned by the Version app
-merge_request:
-author:
diff --git a/changelogs/unreleased/35769-fix-ruby-2-4-compatibility.yml b/changelogs/unreleased/35769-fix-ruby-2-4-compatibility.yml
deleted file mode 100644
index ac480993d85..00000000000
--- a/changelogs/unreleased/35769-fix-ruby-2-4-compatibility.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Issue board when using Ruby 2.4
-merge_request: 13220
-author:
diff --git a/changelogs/unreleased/35793_fix_predicate_names.yml b/changelogs/unreleased/35793_fix_predicate_names.yml
new file mode 100644
index 00000000000..d4da177dc2e
--- /dev/null
+++ b/changelogs/unreleased/35793_fix_predicate_names.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..9fa74884c8a
--- /dev/null
+++ b/changelogs/unreleased/35811-copy-link-note.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for copying permalink to notes via more actions dropdown
+merge_request: 13299
+author:
+type: added
diff --git a/changelogs/unreleased/35815-webhook-log-encoding-error.yml b/changelogs/unreleased/35815-webhook-log-encoding-error.yml
deleted file mode 100644
index 76ec235086c..00000000000
--- a/changelogs/unreleased/35815-webhook-log-encoding-error.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix encoding error for WebHook logging
-merge_request: 13230
-author: Alexander Randa (@randaalex)
diff --git a/changelogs/unreleased/35845-improve-subgroup-creation-permissions.yml b/changelogs/unreleased/35845-improve-subgroup-creation-permissions.yml
new file mode 100644
index 00000000000..eac8dbe23c2
--- /dev/null
+++ b/changelogs/unreleased/35845-improve-subgroup-creation-permissions.yml
@@ -0,0 +1,5 @@
+---
+title: Improves subgroup creation permissions
+merge_request: 13418
+author:
+type: bugifx
diff --git a/changelogs/unreleased/35942-api-binary-encoding.yaml b/changelogs/unreleased/35942-api-binary-encoding.yaml
new file mode 100644
index 00000000000..4f7960d860e
--- /dev/null
+++ b/changelogs/unreleased/35942-api-binary-encoding.yaml
@@ -0,0 +1,3 @@
+---
+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
new file mode 100644
index 00000000000..ce565b177d0
--- /dev/null
+++ b/changelogs/unreleased/35994-archived-projects-only.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..04791e09b84
--- /dev/null
+++ b/changelogs/unreleased/36010-api-v4-allows-setting-a-branch-that-doesn-t-exist-as-the-default-one.yml
@@ -0,0 +1,4 @@
+---
+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
new file mode 100644
index 00000000000..7c5e0a0cd0d
--- /dev/null
+++ b/changelogs/unreleased/36041-notification-title.yml
@@ -0,0 +1,4 @@
+---
+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
new file mode 100644
index 00000000000..9ba75d8b1d0
--- /dev/null
+++ b/changelogs/unreleased/36087-users-cannot-delete-their-account.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..1b664efb8c2
--- /dev/null
+++ b/changelogs/unreleased/36114-stuck-mrs-job-follow-up.yml
@@ -0,0 +1,4 @@
+---
+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
new file mode 100644
index 00000000000..beb01ae5b1a
--- /dev/null
+++ b/changelogs/unreleased/36119-issuable-workers.yml
@@ -0,0 +1,4 @@
+---
+title: Simplify checking if objects exist code in new issaubles workers
+merge_request:
+author:
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
new file mode 100644
index 00000000000..b51b5e58b39
--- /dev/null
+++ b/changelogs/unreleased/36213-return-is_admin-in-users-api-when-current_user-is-admin.yml
@@ -0,0 +1,6 @@
+---
+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/36262_merge_request_reference_in_merge_commit_global.yml b/changelogs/unreleased/36262_merge_request_reference_in_merge_commit_global.yml
new file mode 100644
index 00000000000..356857d6e8a
--- /dev/null
+++ b/changelogs/unreleased/36262_merge_request_reference_in_merge_commit_global.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..1a43c66debd
--- /dev/null
+++ b/changelogs/unreleased/36385-pipeline-graph-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Prevents jobs dropdown from closing in pipeline graph
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/36611-error-in-getcomposer-link.yml b/changelogs/unreleased/36611-error-in-getcomposer-link.yml
new file mode 100644
index 00000000000..1ff6ec01684
--- /dev/null
+++ b/changelogs/unreleased/36611-error-in-getcomposer-link.yml
@@ -0,0 +1,5 @@
+---
+title: Fix external link to Composer website
+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
new file mode 100644
index 00000000000..76c9c905590
--- /dev/null
+++ b/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml
@@ -0,0 +1,5 @@
+---
+title: Normalize styles for empty state combo button
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/36792-inline-user-refresh-when-creating-project.yml b/changelogs/unreleased/36792-inline-user-refresh-when-creating-project.yml
new file mode 100644
index 00000000000..be08da0433a
--- /dev/null
+++ b/changelogs/unreleased/36792-inline-user-refresh-when-creating-project.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..a37de4325bb
--- /dev/null
+++ b/changelogs/unreleased/36807-gc-unwanted-refs-after-import.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..54c7a8c8788
--- /dev/null
+++ b/changelogs/unreleased/36821-fix-new-nav-wrapping-caret-and-increasing-height.yml
@@ -0,0 +1,5 @@
+---
+title: Fix new navigation wrapping and causing height to grow
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml b/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml
new file mode 100644
index 00000000000..e48a5704fdd
--- /dev/null
+++ b/changelogs/unreleased/36859-update-gpg-docs-with-gpg2.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..3e9fcc55836
--- /dev/null
+++ b/changelogs/unreleased/36860-migrate-issues-author.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate issues authored by deleted user to the Ghost user
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/3686_make_tarball_download_url.yml b/changelogs/unreleased/3686_make_tarball_download_url.yml
deleted file mode 100644
index 4e75e52e3ac..00000000000
--- a/changelogs/unreleased/3686_make_tarball_download_url.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: repository archive download url now ends with selected file extension
-merge_request: 13178
-author: haseebeqx
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
new file mode 100644
index 00000000000..a06c84c30e6
--- /dev/null
+++ b/changelogs/unreleased/36882-disable-gitlab-project-import-button-if-source-disabled.yml
@@ -0,0 +1,5 @@
+---
+title: Disable GitLab Project Import Button if source disabled
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/36917-branch-tooltip.yml b/changelogs/unreleased/36917-branch-tooltip.yml
new file mode 100644
index 00000000000..2d37de50cec
--- /dev/null
+++ b/changelogs/unreleased/36917-branch-tooltip.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..06c6105fab6
--- /dev/null
+++ b/changelogs/unreleased/36937-fix-invite-by-email-text.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..b48b10049ed
--- /dev/null
+++ b/changelogs/unreleased/36939-fix-find-blobs-by-path.yml
@@ -0,0 +1,5 @@
+---
+title: Fix searching for files by path
+merge_request: 13798
+author:
+type: fixed
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
new file mode 100644
index 00000000000..83f6b2d21e1
--- /dev/null
+++ b/changelogs/unreleased/36994-toggle-for-automatically-collapsing-outdated-diff-comments.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..fcaa6ec13f8
--- /dev/null
+++ b/changelogs/unreleased/37023-remove-focus-styles-from-dropdown-empty-link.yml
@@ -0,0 +1,5 @@
+---
+title: Remove focus styles from dropdown empty links
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37104-fix-graph-date-format.yml b/changelogs/unreleased/37104-fix-graph-date-format.yml
new file mode 100644
index 00000000000..f7d39fe8283
--- /dev/null
+++ b/changelogs/unreleased/37104-fix-graph-date-format.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect date/time formatting on prometheus graphs
+merge_request: 13865
+author:
+type: fixed
diff --git a/changelogs/unreleased/37147-fix-fallback-emoji-alignment.yml b/changelogs/unreleased/37147-fix-fallback-emoji-alignment.yml
new file mode 100644
index 00000000000..34161e63c81
--- /dev/null
+++ b/changelogs/unreleased/37147-fix-fallback-emoji-alignment.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..f0ffb670102
--- /dev/null
+++ b/changelogs/unreleased/37158-autodevops-banner.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..3ef080b8eae
--- /dev/null
+++ b/changelogs/unreleased/37179-dashboard-project-dropdown.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..ef83dc1d10a
--- /dev/null
+++ b/changelogs/unreleased/37198-api-doesn-t-respect-default-group-visibility.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..593e74593c4
--- /dev/null
+++ b/changelogs/unreleased/37204-deprecate-git-user-manual-ssh-config.yml
@@ -0,0 +1,5 @@
+---
+title: Deprecate custom SSH client configuration for the git user
+merge_request: 13930
+author:
+type: deprecated
diff --git a/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml b/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml
new file mode 100644
index 00000000000..d6d21ac4c51
--- /dev/null
+++ b/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..59bc1bd201e
--- /dev/null
+++ b/changelogs/unreleased/37331-button-MR-widget.yml
@@ -0,0 +1,5 @@
+---
+title: Fix buttons with different height in merge request widget
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37368-blob-viewer-on-mobile.yml b/changelogs/unreleased/37368-blob-viewer-on-mobile.yml
new file mode 100644
index 00000000000..6a955f5715f
--- /dev/null
+++ b/changelogs/unreleased/37368-blob-viewer-on-mobile.yml
@@ -0,0 +1,5 @@
+---
+title: Make blob viewer for rich contents wider for mobile
+merge_request: 14011
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/37406-success-status-icon.yml b/changelogs/unreleased/37406-success-status-icon.yml
new file mode 100644
index 00000000000..faac947f188
--- /dev/null
+++ b/changelogs/unreleased/37406-success-status-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken svg in jobs dropdown for success status
+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
new file mode 100644
index 00000000000..2a8e8a33225
--- /dev/null
+++ b/changelogs/unreleased/37499-add-description-template-examples-to-the-docs.yml
@@ -0,0 +1,5 @@
+---
+title: Add description template examples to documentation
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/5971-webhook-testing.yml b/changelogs/unreleased/5971-webhook-testing.yml
deleted file mode 100644
index 58233091977..00000000000
--- a/changelogs/unreleased/5971-webhook-testing.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow testing any events for project hooks and system hooks
-merge_request: 11728
-author: Alexander Randa (@randaalex)
diff --git a/changelogs/unreleased/add-filter-by-my-reaction.yml b/changelogs/unreleased/add-filter-by-my-reaction.yml
new file mode 100644
index 00000000000..dc1601cf3ee
--- /dev/null
+++ b/changelogs/unreleased/add-filter-by-my-reaction.yml
@@ -0,0 +1,4 @@
+---
+title: Add my reaction filter to search bar
+merge_request: 12962
+author: Hiroyuki Sato
diff --git a/changelogs/unreleased/add-filtered-search-group-issues-ce.yml b/changelogs/unreleased/add-filtered-search-group-issues-ce.yml
deleted file mode 100644
index f83f4173890..00000000000
--- a/changelogs/unreleased/add-filtered-search-group-issues-ce.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add filtered search to group issue dashboard
-merge_request:
-author:
diff --git a/changelogs/unreleased/add-star-for-action-scope.yml b/changelogs/unreleased/add-star-for-action-scope.yml
deleted file mode 100644
index a8119a01ec4..00000000000
--- a/changelogs/unreleased/add-star-for-action-scope.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add star for action scope, in order to delete image from registry
-merge_request: 13248
-author: jean
diff --git a/changelogs/unreleased/add_message_to_the_404_page.yml b/changelogs/unreleased/add_message_to_the_404_page.yml
new file mode 100644
index 00000000000..f567796fe9f
--- /dev/null
+++ b/changelogs/unreleased/add_message_to_the_404_page.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..3e4105a4232
--- /dev/null
+++ b/changelogs/unreleased/add_quick_submission_on_user_settings_page.yml
@@ -0,0 +1,5 @@
+---
+title: Add quick submission on user settings page
+merge_request: 14007
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/changelogs/unreleased/additional-time-series-charts.yml b/changelogs/unreleased/additional-time-series-charts.yml
new file mode 100644
index 00000000000..80c1af54881
--- /dev/null
+++ b/changelogs/unreleased/additional-time-series-charts.yml
@@ -0,0 +1,5 @@
+---
+title: Added support the multiple time series for prometheus monitoring
+merge_request: !36893
+author:
+type: changed
diff --git a/changelogs/unreleased/api-delete-respect-headers.yml b/changelogs/unreleased/api-delete-respect-headers.yml
new file mode 100644
index 00000000000..cfc8fbfdf91
--- /dev/null
+++ b/changelogs/unreleased/api-delete-respect-headers.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..0be35a5823b
--- /dev/null
+++ b/changelogs/unreleased/api-gpg-key-management.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..68d8d3d5168
--- /dev/null
+++ b/changelogs/unreleased/api_branches_head.yml
@@ -0,0 +1,5 @@
+---
+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/artifacts-download-dropdown-menu-is-too-narrow.yml b/changelogs/unreleased/artifacts-download-dropdown-menu-is-too-narrow.yml
deleted file mode 100644
index 7d47c60e262..00000000000
--- a/changelogs/unreleased/artifacts-download-dropdown-menu-is-too-narrow.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Increase width of dropdown menus automatically
-merge_request: 12809
-author: Thomas Wucher
diff --git a/changelogs/unreleased/backstage-gb-after-save-asynchronous-job-hooks.yml b/changelogs/unreleased/backstage-gb-after-save-asynchronous-job-hooks.yml
new file mode 100644
index 00000000000..fd0b7c4f43c
--- /dev/null
+++ b/changelogs/unreleased/backstage-gb-after-save-asynchronous-job-hooks.yml
@@ -0,0 +1,5 @@
+---
+title: Fire hooks asynchronously when creating a new job to improve performance
+merge_request: 13734
+author:
+type: changed
diff --git a/changelogs/unreleased/breadcrumbs-collapsed-title-width-fix.yml b/changelogs/unreleased/breadcrumbs-collapsed-title-width-fix.yml
deleted file mode 100644
index 988fdacb5fd..00000000000
--- a/changelogs/unreleased/breadcrumbs-collapsed-title-width-fix.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed breadcrumbs title aggressively collapsing
-merge_request:
-author:
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
new file mode 100644
index 00000000000..7571999fa75
--- /dev/null
+++ b/changelogs/unreleased/bump-omniauth-ldap-gem-version-2-0-4.yml
@@ -0,0 +1,4 @@
+---
+title: Bumps omniauth-ldap gem version to 2.0.4
+merge_request: 13465
+author:
diff --git a/changelogs/unreleased/bump-omniauth-ldap-gem-version.yml b/changelogs/unreleased/bump-omniauth-ldap-gem-version.yml
deleted file mode 100644
index 42e1c9e8f83..00000000000
--- a/changelogs/unreleased/bump-omniauth-ldap-gem-version.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Prevent LDAP login callback from being called with a GET request
-merge_request: 13059
-author:
diff --git a/changelogs/unreleased/bvl-add-all-settings-to-api.yml b/changelogs/unreleased/bvl-add-all-settings-to-api.yml
deleted file mode 100644
index bfaf237a21c..00000000000
--- a/changelogs/unreleased/bvl-add-all-settings-to-api.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make all application-settings accessible through the API
-merge_request: 12851
-author:
diff --git a/changelogs/unreleased/bvl-free-unused-names.yml b/changelogs/unreleased/bvl-free-unused-names.yml
deleted file mode 100644
index 53acb95e5bb..00000000000
--- a/changelogs/unreleased/bvl-free-unused-names.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Free up some top level words, reject top level groups named like files in the
- public folder
-merge_request: 12932
-author:
diff --git a/changelogs/unreleased/bvl-improve-bare-project-import.yml b/changelogs/unreleased/bvl-improve-bare-project-import.yml
new file mode 100644
index 00000000000..74c1da4ea40
--- /dev/null
+++ b/changelogs/unreleased/bvl-improve-bare-project-import.yml
@@ -0,0 +1,6 @@
+---
+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-nfs-circuitbreaker.yml b/changelogs/unreleased/bvl-nfs-circuitbreaker.yml
deleted file mode 100644
index 151854ed31f..00000000000
--- a/changelogs/unreleased/bvl-nfs-circuitbreaker.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Block access to failing repository storage
-merge_request: 11449
-author:
diff --git a/changelogs/unreleased/bvl-validate-po-files.yml b/changelogs/unreleased/bvl-validate-po-files.yml
new file mode 100644
index 00000000000..f840b2c3973
--- /dev/null
+++ b/changelogs/unreleased/bvl-validate-po-files.yml
@@ -0,0 +1,4 @@
+---
+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
new file mode 100644
index 00000000000..fe3fe3be976
--- /dev/null
+++ b/changelogs/unreleased/cache-issue-and-mr-counts.yml
@@ -0,0 +1,5 @@
+---
+title: Cache the number of open issues and merge requests
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/check-trigger-permissions.yml b/changelogs/unreleased/check-trigger-permissions.yml
new file mode 100644
index 00000000000..e0809cea9bf
--- /dev/null
+++ b/changelogs/unreleased/check-trigger-permissions.yml
@@ -0,0 +1,5 @@
+---
+title: Improve migrations using triggers
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/collapsable-pipeline-settings.yml b/changelogs/unreleased/collapsable-pipeline-settings.yml
new file mode 100644
index 00000000000..d41959f8ab0
--- /dev/null
+++ b/changelogs/unreleased/collapsable-pipeline-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Add collapsable sections for Pipeline Settings
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/diff-changed-files-dropdown.yml b/changelogs/unreleased/diff-changed-files-dropdown.yml
deleted file mode 100644
index 2d2a26ffea2..00000000000
--- a/changelogs/unreleased/diff-changed-files-dropdown.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Moved diff changed files into a dropdown
-merge_request:
-author:
diff --git a/changelogs/unreleased/disable-project-export.yml b/changelogs/unreleased/disable-project-export.yml
new file mode 100644
index 00000000000..d7ca9f46193
--- /dev/null
+++ b/changelogs/unreleased/disable-project-export.yml
@@ -0,0 +1,4 @@
+---
+title: Add option to disable project export on instance
+merge_request: 13211
+author: Robin Bobbitt
diff --git a/changelogs/unreleased/dm-large-push-performance.yml b/changelogs/unreleased/dm-large-push-performance.yml
deleted file mode 100644
index f5fe1bd3b28..00000000000
--- a/changelogs/unreleased/dm-large-push-performance.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Improve performance of large (initial) push into default branch
-merge_request:
-author:
diff --git a/changelogs/unreleased/docs-confidential-issue.yml b/changelogs/unreleased/docs-confidential-issue.yml
new file mode 100644
index 00000000000..841970ef4cf
--- /dev/null
+++ b/changelogs/unreleased/docs-confidential-issue.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..d75c46313f4
--- /dev/null
+++ b/changelogs/unreleased/docs-document-version-for-group-milestones-api.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..db68428fda3
--- /dev/null
+++ b/changelogs/unreleased/docs-fix-15669-issue-move-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add to_project_id parameter to Move Issue via API example
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/docs-update-ci-docker-using-docker-images.yml b/changelogs/unreleased/docs-update-ci-docker-using-docker-images.yml
new file mode 100644
index 00000000000..d8a5073f110
--- /dev/null
+++ b/changelogs/unreleased/docs-update-ci-docker-using-docker-images.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..a7db18dbd60
--- /dev/null
+++ b/changelogs/unreleased/dont-remove-add-diff-btn-on-post.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed add diff note button not showing after deleting a comment
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dont-use-limit-offset-when-counting-projects.yml b/changelogs/unreleased/dont-use-limit-offset-when-counting-projects.yml
deleted file mode 100644
index 8ecea635ce5..00000000000
--- a/changelogs/unreleased/dont-use-limit-offset-when-counting-projects.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: "Improve performance of checking for projects on the projects dashboard"
-merge_request:
-author:
diff --git a/changelogs/unreleased/dz-fix-calendar-today.yml b/changelogs/unreleased/dz-fix-calendar-today.yml
deleted file mode 100644
index 5320d8b26b5..00000000000
--- a/changelogs/unreleased/dz-fix-calendar-today.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix today day highlight in calendar
-merge_request: 13048
-author:
diff --git a/changelogs/unreleased/eager-load-project-creators-for-project-dashboards.yml b/changelogs/unreleased/eager-load-project-creators-for-project-dashboards.yml
deleted file mode 100644
index e550e0b2f44..00000000000
--- a/changelogs/unreleased/eager-load-project-creators-for-project-dashboards.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Eager load project creators for project dashboards
-merge_request:
-author:
diff --git a/changelogs/unreleased/enable-scss-lint-bang-format.yml b/changelogs/unreleased/enable-scss-lint-bang-format.yml
deleted file mode 100644
index 0b73760198e..00000000000
--- a/changelogs/unreleased/enable-scss-lint-bang-format.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Enable BangFormat in scss-lint [ci skip]
-merge_request: 12815
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/enable-scss-lint-declaration-order.yml b/changelogs/unreleased/enable-scss-lint-declaration-order.yml
deleted file mode 100644
index 7ac2f55592e..00000000000
--- a/changelogs/unreleased/enable-scss-lint-declaration-order.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Enable DeclarationOrder in scss-lint
-merge_request: 12805
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/enable-scss-lint-import-path.yml b/changelogs/unreleased/enable-scss-lint-import-path.yml
deleted file mode 100644
index d158cf5b5f3..00000000000
--- a/changelogs/unreleased/enable-scss-lint-import-path.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Enable ImportPath in scss-lint
-merge_request: 12749
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/enable-scss-lint-property-spelling.yml b/changelogs/unreleased/enable-scss-lint-property-spelling.yml
deleted file mode 100644
index c5a5a4dddb6..00000000000
--- a/changelogs/unreleased/enable-scss-lint-property-spelling.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Enable PropertySpelling in scss-lint
-merge_request: 12752
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/enable-scss-lint-space-after-comma.yml b/changelogs/unreleased/enable-scss-lint-space-after-comma.yml
deleted file mode 100644
index 210f34fbb87..00000000000
--- a/changelogs/unreleased/enable-scss-lint-space-after-comma.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Enable SpaceAfterComma in scss-lint
-merge_request: 12734
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/enable-scss-lint-unnecessary-parent-reference.yml b/changelogs/unreleased/enable-scss-lint-unnecessary-parent-reference.yml
deleted file mode 100644
index 59d5df56525..00000000000
--- a/changelogs/unreleased/enable-scss-lint-unnecessary-parent-reference.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Enable UnnecessaryParentReference in scss-lint
-merge_request: 12738
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/ericy_ts-protected_branches_api.yml b/changelogs/unreleased/ericy_ts-protected_branches_api.yml
deleted file mode 100644
index 4cd275c5e8f..00000000000
--- a/changelogs/unreleased/ericy_ts-protected_branches_api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add API for protected branches to allow for wildcard matching and no access
- restrictions
-merge_request: 12756
-author: Eric Yu
diff --git a/changelogs/unreleased/events-migration-cleanup.yml b/changelogs/unreleased/events-migration-cleanup.yml
new file mode 100644
index 00000000000..1e3e843f252
--- /dev/null
+++ b/changelogs/unreleased/events-migration-cleanup.yml
@@ -0,0 +1,5 @@
+---
+title: Finish migration to the new events setup
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/feature-backup-custom-path.yml b/changelogs/unreleased/feature-backup-custom-path.yml
deleted file mode 100644
index 1c5f25b3ee5..00000000000
--- a/changelogs/unreleased/feature-backup-custom-path.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Support custom directory in gitlab:backup:create task
-merge_request: 12984
-author: Markus Koller
diff --git a/changelogs/unreleased/feature-dependency-status-badge.yml b/changelogs/unreleased/feature-dependency-status-badge.yml
new file mode 100644
index 00000000000..1becff3585a
--- /dev/null
+++ b/changelogs/unreleased/feature-dependency-status-badge.yml
@@ -0,0 +1,5 @@
+---
+title: Add badge for dependency status
+merge_request: 13588
+author: Markus Koller
+type: other
diff --git a/changelogs/unreleased/feature-gb-auto-retry-failed-ci-job.yml b/changelogs/unreleased/feature-gb-auto-retry-failed-ci-job.yml
deleted file mode 100644
index bdafc5929c0..00000000000
--- a/changelogs/unreleased/feature-gb-auto-retry-failed-ci-job.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow to configure automatic retry of a failed CI/CD job
-merge_request: 12909
-author:
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
new file mode 100644
index 00000000000..920679ca166
--- /dev/null
+++ b/changelogs/unreleased/feature-gb-download-single-job-artifact-using-api.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..00c38a0c671
--- /dev/null
+++ b/changelogs/unreleased/feature-gb-kubernetes-only-pipeline-jobs.yml
@@ -0,0 +1,5 @@
+---
+title: Add CI/CD active kubernetes job policy
+merge_request: 13849
+author:
+type: added
diff --git a/changelogs/unreleased/feature-gpg-signed-commits.yml b/changelogs/unreleased/feature-gpg-signed-commits.yml
deleted file mode 100644
index 99bc5a309ef..00000000000
--- a/changelogs/unreleased/feature-gpg-signed-commits.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: GPG signed commits integration
-merge_request: 9546
-author: Alexis Reigel
diff --git a/changelogs/unreleased/feature-gpg-verification-status.yml b/changelogs/unreleased/feature-gpg-verification-status.yml
new file mode 100644
index 00000000000..7518fafcdb8
--- /dev/null
+++ b/changelogs/unreleased/feature-gpg-verification-status.yml
@@ -0,0 +1,6 @@
+---
+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
new file mode 100644
index 00000000000..b885029f589
--- /dev/null
+++ b/changelogs/unreleased/feature-plantuml-restructured-text.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..b57b9a3dfbe
--- /dev/null
+++ b/changelogs/unreleased/feature-sm-33281-protected-runner-executes-jobs-on-protected-branch.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..969a5aeaed3
--- /dev/null
+++ b/changelogs/unreleased/feature-sm-34518-extend-api-pipeline-schedule-variable-new.yml
@@ -0,0 +1,5 @@
+---
+title: 'Extend API: Pipeline Schedule Variable'
+merge_request: 13653
+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
new file mode 100644
index 00000000000..006b0b45844
--- /dev/null
+++ b/changelogs/unreleased/feature-sm-37239-implement-failure_reason-on-ci_builds.yml
@@ -0,0 +1,5 @@
+---
+title: Implement `failure_reason` on `ci_builds`
+merge_request: 13937
+author:
+type: added
diff --git a/changelogs/unreleased/fix-500-error-when-rendering-avatar-for-deleted-project-creator.yml b/changelogs/unreleased/fix-500-error-when-rendering-avatar-for-deleted-project-creator.yml
deleted file mode 100644
index be6f1ea00fb..00000000000
--- a/changelogs/unreleased/fix-500-error-when-rendering-avatar-for-deleted-project-creator.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Modify if condition to be more readable
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-btn-alignment.yml b/changelogs/unreleased/fix-btn-alignment.yml
new file mode 100644
index 00000000000..e5dce3d3a0e
--- /dev/null
+++ b/changelogs/unreleased/fix-btn-alignment.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..8550f3e3c1b
--- /dev/null
+++ b/changelogs/unreleased/fix-edit-merge-request-button-case.yml
@@ -0,0 +1,5 @@
+---
+title: Fix edit merge request and issues button inconsistent letter casing
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-gb-handle-max-pages-artifacts-size-correctly.yml b/changelogs/unreleased/fix-gb-handle-max-pages-artifacts-size-correctly.yml
deleted file mode 100644
index 3d9592bbf2a..00000000000
--- a/changelogs/unreleased/fix-gb-handle-max-pages-artifacts-size-correctly.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Handle maximum pages artifacts size correctly
-merge_request: 13072
-author:
diff --git a/changelogs/unreleased/fix-group-milestone-link-in-issuable-sidebar.yml b/changelogs/unreleased/fix-group-milestone-link-in-issuable-sidebar.yml
deleted file mode 100644
index 1558e575e6d..00000000000
--- a/changelogs/unreleased/fix-group-milestone-link-in-issuable-sidebar.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix links to group milestones from issue and merge request sidebar
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-import-export-performance.yml b/changelogs/unreleased/fix-import-export-performance.yml
new file mode 100644
index 00000000000..1f59c4eb179
--- /dev/null
+++ b/changelogs/unreleased/fix-import-export-performance.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..4e9cf7faae8
--- /dev/null
+++ b/changelogs/unreleased/fix-import-fork-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Import/Export issue to do with fork merge requests
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-npm-security-updates.yml b/changelogs/unreleased/fix-npm-security-updates.yml
new file mode 100644
index 00000000000..faa0c3149b8
--- /dev/null
+++ b/changelogs/unreleased/fix-npm-security-updates.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade brace-expansion NPM package due to security issue
+merge_request: 13665
+author: Markus Koller
+type: security
diff --git a/changelogs/unreleased/fix-oauth-checkboxes.yml b/changelogs/unreleased/fix-oauth-checkboxes.yml
deleted file mode 100644
index 2839ccc42cb..00000000000
--- a/changelogs/unreleased/fix-oauth-checkboxes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed sign-in restrictions buttons not toggling active state
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-replying-to-commit-comment-in-mr-from-fork.yml b/changelogs/unreleased/fix-replying-to-commit-comment-in-mr-from-fork.yml
deleted file mode 100644
index f4136460626..00000000000
--- a/changelogs/unreleased/fix-replying-to-commit-comment-in-mr-from-fork.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix replying to commit comments on merge requests created from forks
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml b/changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml
deleted file mode 100644
index ddaec4f19f9..00000000000
--- a/changelogs/unreleased/fix-sm-34547-cannot-connect-to-ci-server-error-messages.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix an order of operations for CI connection error message in merge request
- widget
-merge_request: 13252
-author:
diff --git a/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml b/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml
deleted file mode 100644
index 07840205b6e..00000000000
--- a/changelogs/unreleased/fix-sm-35931-active-ci-pipelineschedule-have-nullified-next_run_at.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix pipeline_schedules pages when active schedule has an abnormal state
-merge_request: 13286
-author:
diff --git a/changelogs/unreleased/fix-stray-or-in-project-create-ui.yml b/changelogs/unreleased/fix-stray-or-in-project-create-ui.yml
new file mode 100644
index 00000000000..ae4df3ee31a
--- /dev/null
+++ b/changelogs/unreleased/fix-stray-or-in-project-create-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Fix stray OR in New Project page
+merge_request: 14096
+author: Robin Bobbitt
+type: fixed
diff --git a/changelogs/unreleased/fix_typo_in_deploy_keys_docs.yml b/changelogs/unreleased/fix_typo_in_deploy_keys_docs.yml
new file mode 100644
index 00000000000..fa50e36e28a
--- /dev/null
+++ b/changelogs/unreleased/fix_typo_in_deploy_keys_docs.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..60da2e455f2
--- /dev/null
+++ b/changelogs/unreleased/fix_wiki_toc_indent.yml
@@ -0,0 +1,5 @@
+---
+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/fixes-for-internal-auth-disabled.yml b/changelogs/unreleased/fixes-for-internal-auth-disabled.yml
deleted file mode 100644
index 188d2770455..00000000000
--- a/changelogs/unreleased/fixes-for-internal-auth-disabled.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixes needed when GitLab sign-in is not enabled
-merge_request: 12491
-author: Robin Bobbitt
diff --git a/changelogs/unreleased/font-weight-adjusted.yml b/changelogs/unreleased/font-weight-adjusted.yml
new file mode 100644
index 00000000000..827f3485099
--- /dev/null
+++ b/changelogs/unreleased/font-weight-adjusted.yml
@@ -0,0 +1,5 @@
+---
+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/fuzzy-issue-search.yml b/changelogs/unreleased/fuzzy-issue-search.yml
new file mode 100644
index 00000000000..8195e97ed59
--- /dev/null
+++ b/changelogs/unreleased/fuzzy-issue-search.yml
@@ -0,0 +1,5 @@
+---
+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/gitaly-feature-toggles-development-opt-out.yml b/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml
new file mode 100644
index 00000000000..90c25d782c8
--- /dev/null
+++ b/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..edf11484d1f
--- /dev/null
+++ b/changelogs/unreleased/gitaly-post-upload-pack-mandatory.yml
@@ -0,0 +1,5 @@
+---
+title: Make Gitaly PostUploadPack mandatory
+merge_request: 13953
+author:
+type: changed
diff --git a/changelogs/unreleased/gitaly_ref_exists.yml b/changelogs/unreleased/gitaly_ref_exists.yml
new file mode 100644
index 00000000000..f62b646e406
--- /dev/null
+++ b/changelogs/unreleased/gitaly_ref_exists.yml
@@ -0,0 +1,4 @@
+---
+title: Implement the Gitaly RefService::RefExists endpoint
+merge_request: 13528
+author: Andrew Newdigate
diff --git a/changelogs/unreleased/github.yml b/changelogs/unreleased/github.yml
deleted file mode 100644
index 585b9b13b65..00000000000
--- a/changelogs/unreleased/github.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Reduce memory usage of the GitHub importer
-merge_request: 12886
-author:
diff --git a/changelogs/unreleased/group-milestone-references-system-notes.yml b/changelogs/unreleased/group-milestone-references-system-notes.yml
deleted file mode 100644
index 58215352305..00000000000
--- a/changelogs/unreleased/group-milestone-references-system-notes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Support Markdown references, autocomplete, and quick actions for group milestones
-merge_request:
-author:
diff --git a/changelogs/unreleased/group-mr-search-bar.yml b/changelogs/unreleased/group-mr-search-bar.yml
new file mode 100644
index 00000000000..0b554a5d7c9
--- /dev/null
+++ b/changelogs/unreleased/group-mr-search-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Add filtered search to group merge requests dashboard
+merge_request: 13688
+author: Hiroyuki Sato
+type: changed
diff --git a/changelogs/unreleased/group-new-issue.yml b/changelogs/unreleased/group-new-issue.yml
deleted file mode 100644
index 5480a44526b..00000000000
--- a/changelogs/unreleased/group-new-issue.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Cache recent projects for group-level new resource creation.
-merge_request: !13058
-author:
diff --git a/changelogs/unreleased/handle-reserved-words-for-oauth-usernames.yml b/changelogs/unreleased/handle-reserved-words-for-oauth-usernames.yml
deleted file mode 100644
index 0d64844a2b8..00000000000
--- a/changelogs/unreleased/handle-reserved-words-for-oauth-usernames.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Uniquify reserved word usernames on OAuth user creation
-merge_request: 13244
-author: Robin Bobbitt
diff --git a/changelogs/unreleased/improve-autocomplete-user-performance.yml b/changelogs/unreleased/improve-autocomplete-user-performance.yml
new file mode 100644
index 00000000000..5a7153771ff
--- /dev/null
+++ b/changelogs/unreleased/improve-autocomplete-user-performance.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..ec0d1b245e4
--- /dev/null
+++ b/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml
@@ -0,0 +1,6 @@
+---
+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/issue-api-my-reaction.yml b/changelogs/unreleased/issue-api-my-reaction.yml
new file mode 100644
index 00000000000..1c12478fbc0
--- /dev/null
+++ b/changelogs/unreleased/issue-api-my-reaction.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..5e042de7000
--- /dev/null
+++ b/changelogs/unreleased/issue-boards-breadcrumbs-container.yml
@@ -0,0 +1,5 @@
+---
+title: Fix breadcrumbs container in issue boards
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/mattermost_fixes.yml b/changelogs/unreleased/mattermost_fixes.yml
deleted file mode 100644
index 667109a0bb4..00000000000
--- a/changelogs/unreleased/mattermost_fixes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix Mattermost integration
-merge_request:
-author:
diff --git a/changelogs/unreleased/memoize-user-personal-projects-count.yml b/changelogs/unreleased/memoize-user-personal-projects-count.yml
deleted file mode 100644
index 3839a97f185..00000000000
--- a/changelogs/unreleased/memoize-user-personal-projects-count.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Memoize the number of personal projects a user has to reduce COUNT queries
-merge_request:
-author:
diff --git a/changelogs/unreleased/merge-issuable-reopened-into-opened-state.yml b/changelogs/unreleased/merge-issuable-reopened-into-opened-state.yml
deleted file mode 100644
index 5d7af8971e5..00000000000
--- a/changelogs/unreleased/merge-issuable-reopened-into-opened-state.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Merge issuable "reopened" state into "opened"
-merge_request:
-author:
diff --git a/changelogs/unreleased/mk-default-ldap-verify-certificates-secure.yml b/changelogs/unreleased/mk-default-ldap-verify-certificates-secure.yml
new file mode 100644
index 00000000000..865b57fb284
--- /dev/null
+++ b/changelogs/unreleased/mk-default-ldap-verify-certificates-secure.yml
@@ -0,0 +1,5 @@
+---
+title: Default LDAP config "verify_certificates" to true for security
+merge_request: 13915
+author:
+type: changed
diff --git a/changelogs/unreleased/mk-fix-deploy-key-deletion.yml b/changelogs/unreleased/mk-fix-deploy-key-deletion.yml
deleted file mode 100644
index 9ff2e49b14c..00000000000
--- a/changelogs/unreleased/mk-fix-deploy-key-deletion.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix deletion of deploy keys linked to other projects
-merge_request: 13162
-author:
diff --git a/changelogs/unreleased/mk-fix-wiki-backup.yml b/changelogs/unreleased/mk-fix-wiki-backup.yml
deleted file mode 100644
index ba9c1e85955..00000000000
--- a/changelogs/unreleased/mk-fix-wiki-backup.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix improperly skipped backups of wikis.
-merge_request: 13096
-author:
diff --git a/changelogs/unreleased/move-action.yml b/changelogs/unreleased/move-action.yml
new file mode 100644
index 00000000000..65eceae3ef9
--- /dev/null
+++ b/changelogs/unreleased/move-action.yml
@@ -0,0 +1,4 @@
+---
+title: Allow users to move issues to other projects using a / command
+merge_request: 13436
+author: Manolis Mavrofidis
diff --git a/changelogs/unreleased/mr-branch-link-use-tree.yml b/changelogs/unreleased/mr-branch-link-use-tree.yml
deleted file mode 100644
index f4c4d9f5082..00000000000
--- a/changelogs/unreleased/mr-branch-link-use-tree.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: MR branch link now links to tree instead of commits
-merge_request:
-author:
diff --git a/changelogs/unreleased/mr-index-page-performance.yml b/changelogs/unreleased/mr-index-page-performance.yml
new file mode 100644
index 00000000000..df5f44c04fa
--- /dev/null
+++ b/changelogs/unreleased/mr-index-page-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Re-use issue/MR counts for the pagination system
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/pass-before-script-as-is.yml b/changelogs/unreleased/pass-before-script-as-is.yml
deleted file mode 100644
index ac6513dcff6..00000000000
--- a/changelogs/unreleased/pass-before-script-as-is.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Pass before_script and script as-is preserving arrays
-merge_request:
-author:
diff --git a/changelogs/unreleased/pawel-add-sidekiq-metrics-endpoint-32145.yml b/changelogs/unreleased/pawel-add-sidekiq-metrics-endpoint-32145.yml
deleted file mode 100644
index 71eabdc16d2..00000000000
--- a/changelogs/unreleased/pawel-add-sidekiq-metrics-endpoint-32145.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add Prometheus metrics exporter to Sidekiq
-merge_request: 13082
-author:
diff --git a/changelogs/unreleased/pawel-add_more_variables_to_additional_metrics-35267.yml b/changelogs/unreleased/pawel-add_more_variables_to_additional_metrics-35267.yml
deleted file mode 100644
index c1e831306df..00000000000
--- a/changelogs/unreleased/pawel-add_more_variables_to_additional_metrics-35267.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add support for kube_namespace in Metrics queries
-merge_request: 16169
-author:
diff --git a/changelogs/unreleased/perf-slow-issuable.yml b/changelogs/unreleased/perf-slow-issuable.yml
new file mode 100644
index 00000000000..29d15be1401
--- /dev/null
+++ b/changelogs/unreleased/perf-slow-issuable.yml
@@ -0,0 +1,6 @@
+---
+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/post-upload-pack-opt-out.yml b/changelogs/unreleased/post-upload-pack-opt-out.yml
deleted file mode 100644
index 302a99795a0..00000000000
--- a/changelogs/unreleased/post-upload-pack-opt-out.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Enable gitaly_post_upload_pack by default
-merge_request: 13078
-author:
diff --git a/changelogs/unreleased/project-foreign-keys-without-errors.yml b/changelogs/unreleased/project-foreign-keys-without-errors.yml
deleted file mode 100644
index 63c53c8ad8f..00000000000
--- a/changelogs/unreleased/project-foreign-keys-without-errors.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Change project FK migration to skip existing FKs
-merge_request:
-author:
diff --git a/changelogs/unreleased/rc-fix-branches-api-endpoint.yml b/changelogs/unreleased/rc-fix-branches-api-endpoint.yml
deleted file mode 100644
index b36663bbe91..00000000000
--- a/changelogs/unreleased/rc-fix-branches-api-endpoint.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix the /projects/:id/repository/branches endpoint to handle dots in the branch
- name when the project full path contains a `/`
-merge_request: 13115
-author:
diff --git a/changelogs/unreleased/rc-fix-commits-api.yml b/changelogs/unreleased/rc-fix-commits-api.yml
deleted file mode 100644
index 215429eaf6b..00000000000
--- a/changelogs/unreleased/rc-fix-commits-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix the /projects/:id/repository/commits endpoint to handle dots in the ref
- name when the project full path contains a `/`
-merge_request: 13370
-author:
diff --git a/changelogs/unreleased/rc-fix-tags-api.yml b/changelogs/unreleased/rc-fix-tags-api.yml
deleted file mode 100644
index 0a7dd5ca6ab..00000000000
--- a/changelogs/unreleased/rc-fix-tags-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix the /projects/:id/repository/tags endpoint to handle dots in the tag name
- when the project full path contains a `/`
-merge_request: 13368
-author:
diff --git a/changelogs/unreleased/remove-nprogress-gleaning.yml b/changelogs/unreleased/remove-nprogress-gleaning.yml
deleted file mode 100644
index 78e4dc82dd4..00000000000
--- a/changelogs/unreleased/remove-nprogress-gleaning.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove CSS for nprogress removed
-merge_request: 12737
-author: Takuya Noguchi
diff --git a/changelogs/unreleased/remove-redundant-query-when-retrieving-recent-pushes.yml b/changelogs/unreleased/remove-redundant-query-when-retrieving-recent-pushes.yml
deleted file mode 100644
index 83934217e6a..00000000000
--- a/changelogs/unreleased/remove-redundant-query-when-retrieving-recent-pushes.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove redundant query when retrieving the most recent push of a user
-merge_request:
-author:
diff --git a/changelogs/unreleased/reorganise-issues-indexes-for-sorting.yml b/changelogs/unreleased/reorganise-issues-indexes-for-sorting.yml
deleted file mode 100644
index 5bfe55e562f..00000000000
--- a/changelogs/unreleased/reorganise-issues-indexes-for-sorting.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Re-organise "issues" indexes for faster ordering
-merge_request:
-author:
diff --git a/changelogs/unreleased/replace_spinach_search_code-feature.yml b/changelogs/unreleased/replace_spinach_search_code-feature.yml
new file mode 100644
index 00000000000..28d2108c871
--- /dev/null
+++ b/changelogs/unreleased/replace_spinach_search_code-feature.yml
@@ -0,0 +1,5 @@
+---
+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_spec_browse_files.yml b/changelogs/unreleased/replace_spinach_spec_browse_files.yml
deleted file mode 100644
index 7380d39fa9f..00000000000
--- a/changelogs/unreleased/replace_spinach_spec_browse_files.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Replace 'browse_files.feature' spinach test with an rspec analog
-merge_request: 12251
-author: @blackst0ne
diff --git a/changelogs/unreleased/replace_spinach_star-feature.yml b/changelogs/unreleased/replace_spinach_star-feature.yml
new file mode 100644
index 00000000000..6a058691fe5
--- /dev/null
+++ b/changelogs/unreleased/replace_spinach_star-feature.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..36248c54d99
--- /dev/null
+++ b/changelogs/unreleased/replace_spinach_user_lookup-feature.yml
@@ -0,0 +1,5 @@
+---
+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/request-store-wrap.yml b/changelogs/unreleased/request-store-wrap.yml
deleted file mode 100644
index 8017054b77b..00000000000
--- a/changelogs/unreleased/request-store-wrap.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add RequestCache which makes caching with RequestStore easier
-merge_request: 12920
-author:
diff --git a/changelogs/unreleased/restrict-haml-javascript.yml b/changelogs/unreleased/restrict-haml-javascript.yml
deleted file mode 100644
index 3d0a52f416d..00000000000
--- a/changelogs/unreleased/restrict-haml-javascript.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add custom linter for inline JavaScript to haml_lint
-merge_request: 9742
-author: winniehell
diff --git a/changelogs/unreleased/rouge-2-2-0.yml b/changelogs/unreleased/rouge-2-2-0.yml
new file mode 100644
index 00000000000..0b53cd14628
--- /dev/null
+++ b/changelogs/unreleased/rouge-2-2-0.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..2d8879e5574
--- /dev/null
+++ b/changelogs/unreleased/rouge-2-2-1.yml
@@ -0,0 +1,5 @@
+---
+title: Bump rouge to v2.2.1
+merge_request: 13887
+author:
+type: other
diff --git a/changelogs/unreleased/search-flickering.yml b/changelogs/unreleased/search-flickering.yml
deleted file mode 100644
index 951a5a0292a..00000000000
--- a/changelogs/unreleased/search-flickering.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix search box losing focus when typing
-merge_request:
-author:
diff --git a/changelogs/unreleased/seven-days-cycle-analytics.yml b/changelogs/unreleased/seven-days-cycle-analytics.yml
new file mode 100644
index 00000000000..ff660bdd603
--- /dev/null
+++ b/changelogs/unreleased/seven-days-cycle-analytics.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..eaf6cb045d5
--- /dev/null
+++ b/changelogs/unreleased/sh-add-grape-logging.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..d76b688caac
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-jira-gem.yml
@@ -0,0 +1,5 @@
+---
+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-structured-logging.yml b/changelogs/unreleased/sh-structured-logging.yml
deleted file mode 100644
index d89eb93f689..00000000000
--- a/changelogs/unreleased/sh-structured-logging.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add structured logging for Rails processes
-merge_request:
-author:
diff --git a/changelogs/unreleased/sidebar-cache-updates.yml b/changelogs/unreleased/sidebar-cache-updates.yml
new file mode 100644
index 00000000000..aebe53ba5b2
--- /dev/null
+++ b/changelogs/unreleased/sidebar-cache-updates.yml
@@ -0,0 +1,5 @@
+---
+title: Only update the sidebar count caches when needed
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/skip-oauth-authorization-for-trusted-applications.yml b/changelogs/unreleased/skip-oauth-authorization-for-trusted-applications.yml
deleted file mode 100644
index 7b4ae355978..00000000000
--- a/changelogs/unreleased/skip-oauth-authorization-for-trusted-applications.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Skip oAuth authorization for trusted applications
-merge_request:
-author:
diff --git a/changelogs/unreleased/sm-cherry-pick-list-commits-in-message.yml b/changelogs/unreleased/sm-cherry-pick-list-commits-in-message.yml
new file mode 100644
index 00000000000..602ca358b8b
--- /dev/null
+++ b/changelogs/unreleased/sm-cherry-pick-list-commits-in-message.yml
@@ -0,0 +1,5 @@
+---
+title: Add 'from commit' information to cherry-picked commits
+merge_request: 13475
+author: Saverio Miroddi
+type: added
diff --git a/changelogs/unreleased/tc-api-root-merge-requests.yml b/changelogs/unreleased/tc-api-root-merge-requests.yml
deleted file mode 100644
index 17456f943eb..00000000000
--- a/changelogs/unreleased/tc-api-root-merge-requests.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add top-level merge_requests API endpoint
-merge_request: 13060
-author:
diff --git a/changelogs/unreleased/tc-fix-wildcard-protected-delete-merged.yml b/changelogs/unreleased/tc-fix-wildcard-protected-delete-merged.yml
deleted file mode 100644
index 9ca5f81cf79..00000000000
--- a/changelogs/unreleased/tc-fix-wildcard-protected-delete-merged.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make Delete Merged Branches handle wildcard protected branches correctly
-merge_request: 13251
-author:
diff --git a/changelogs/unreleased/tc-issue-api-assignee.yml b/changelogs/unreleased/tc-issue-api-assignee.yml
deleted file mode 100644
index 8d6360d5baf..00000000000
--- a/changelogs/unreleased/tc-issue-api-assignee.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add author_id & assignee_id param to /issues API
-merge_request: 13004
-author:
diff --git a/changelogs/unreleased/tc-no-todo-service-select.yml b/changelogs/unreleased/tc-no-todo-service-select.yml
deleted file mode 100644
index ddcae334aa7..00000000000
--- a/changelogs/unreleased/tc-no-todo-service-select.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Avoid plucking Todo ids in TodoService
-merge_request: 10845
-author:
diff --git a/changelogs/unreleased/tc-remove-nonexisting-namespace-pending-delete-projects.yml b/changelogs/unreleased/tc-remove-nonexisting-namespace-pending-delete-projects.yml
new file mode 100644
index 00000000000..218336df5d2
--- /dev/null
+++ b/changelogs/unreleased/tc-remove-nonexisting-namespace-pending-delete-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Migration to remove pending delete projects with non-existing namespace
+merge_request: 13598
+author:
+type: other
diff --git a/changelogs/unreleased/toggle-new-project-import-description.yml b/changelogs/unreleased/toggle-new-project-import-description.yml
deleted file mode 100644
index 8f0d09e0540..00000000000
--- a/changelogs/unreleased/toggle-new-project-import-description.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Toggle import description with import_sources_enabled
-merge_request: 12691
-author: Brianna Kicia \ No newline at end of file
diff --git a/changelogs/unreleased/url-sanitizer-fixes.yml b/changelogs/unreleased/url-sanitizer-fixes.yml
new file mode 100644
index 00000000000..769036c829c
--- /dev/null
+++ b/changelogs/unreleased/url-sanitizer-fixes.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..0c3acce1455
--- /dev/null
+++ b/changelogs/unreleased/use_full_path_in_project_avatar_url_webhook.yml
@@ -0,0 +1,5 @@
+---
+title: Use full path of project's avatar in webhooks
+merge_request: 13649
+author: Vitaliy @blackst0ne Klachkov
+type: changed
diff --git a/changelogs/unreleased/wiki_api.yml b/changelogs/unreleased/wiki_api.yml
new file mode 100644
index 00000000000..9d60356aedc
--- /dev/null
+++ b/changelogs/unreleased/wiki_api.yml
@@ -0,0 +1,5 @@
+---
+title: Add API support for wiki pages
+merge_request: 13372
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/changelogs/unreleased/wiki_title.yml b/changelogs/unreleased/wiki_title.yml
deleted file mode 100644
index 3ef5fa2969b..00000000000
--- a/changelogs/unreleased/wiki_title.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow wiki pages to be renamed in the UI
-merge_request: 10069
-author: wendy0402
diff --git a/changelogs/unreleased/winh-derive-project-name.yml b/changelogs/unreleased/winh-derive-project-name.yml
deleted file mode 100644
index 2244d21d768..00000000000
--- a/changelogs/unreleased/winh-derive-project-name.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Derive project path from import URL
-merge_request: 13131
-author:
diff --git a/changelogs/unreleased/winh-dropdown-changelog-docs.yml b/changelogs/unreleased/winh-dropdown-changelog-docs.yml
new file mode 100644
index 00000000000..2f42b4dd9f9
--- /dev/null
+++ b/changelogs/unreleased/winh-dropdown-changelog-docs.yml
@@ -0,0 +1,5 @@
+---
+title: Restyle dropdown menus to make them look consistent
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/zj-add-pipeline-source-variable.yml b/changelogs/unreleased/zj-add-pipeline-source-variable.yml
new file mode 100644
index 00000000000..5d98cd8086a
--- /dev/null
+++ b/changelogs/unreleased/zj-add-pipeline-source-variable.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..f1a004ebd19
--- /dev/null
+++ b/changelogs/unreleased/zj-auto-devops-table.yml
@@ -0,0 +1,5 @@
+---
+title: Allow users and administrator to configure Auto-DevOps
+merge_request: 13923
+author:
+type: added
diff --git a/changelogs/unreleased/zj-delete-mm-team.yml b/changelogs/unreleased/zj-delete-mm-team.yml
deleted file mode 100644
index f0c782c4566..00000000000
--- a/changelogs/unreleased/zj-delete-mm-team.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Remove Mattermost team when deleting a group
-merge_request: 11362
-author:
diff --git a/changelogs/unreleased/zj-disable-pages-in-subgroups.yml b/changelogs/unreleased/zj-disable-pages-in-subgroups.yml
new file mode 100644
index 00000000000..22c36214e1f
--- /dev/null
+++ b/changelogs/unreleased/zj-disable-pages-in-subgroups.yml
@@ -0,0 +1,5 @@
+---
+title: Remove pages settings when not available
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-pipeline-badge-improvements.yml b/changelogs/unreleased/zj-pipeline-badge-improvements.yml
deleted file mode 100644
index 735192ede2d..00000000000
--- a/changelogs/unreleased/zj-pipeline-badge-improvements.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Update build badges to be pipeline badges and display passing instead of success
-merge_request:
-author:
diff --git a/changelogs/unreleased/zj-project-templates.yml b/changelogs/unreleased/zj-project-templates.yml
deleted file mode 100644
index ab6e0f2d5f2..00000000000
--- a/changelogs/unreleased/zj-project-templates.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Projects can be created from templates
-merge_request: 13108
-author:
diff --git a/changelogs/unreleased/zj-remove-ci-api-v1.yml b/changelogs/unreleased/zj-remove-ci-api-v1.yml
new file mode 100644
index 00000000000..8f2dc321b36
--- /dev/null
+++ b/changelogs/unreleased/zj-remove-ci-api-v1.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..474392a8cdd
--- /dev/null
+++ b/changelogs/unreleased/zj-reword-job-to-pipeline-chart-view.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..443c4355890
--- /dev/null
+++ b/changelogs/unreleased/zj-sort-templates.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..5464f0e3d42
--- /dev/null
+++ b/changelogs/unreleased/zj-update-rails-template.yml
@@ -0,0 +1,5 @@
+---
+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
new file mode 100644
index 00000000000..daa6a234c07
--- /dev/null
+++ b/changelogs/unreleased/zj-upgrade-grape.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade grape to 1.0
+merge_request:
+author:
+type: other
diff --git a/config/application.rb b/config/application.rb
index 47887bf8596..32a290f2002 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -51,31 +51,24 @@ module Gitlab
# Configure sensitive parameters which will be filtered from the log file.
#
# Parameters filtered:
- # - Password (:password, :password_confirmation)
- # - Private tokens
+ # - Any parameter ending with `_token`
+ # - Any parameter containing `password`
+ # - Any parameter containing `secret`
# - Two-factor tokens (:otp_attempt)
# - Repo/Project Import URLs (:import_url)
# - Build variables (:variables)
# - GitLab Pages SSL cert/key info (:certificate, :encrypted_key)
# - Webhook URLs (:hook)
- # - GitLab-shell secret token (:secret_token)
# - Sentry DSN (:sentry_dsn)
# - Deploy keys (:key)
+ config.filter_parameters += [/_token$/, /password/, /secret/]
config.filter_parameters += %i(
- authentication_token
certificate
encrypted_key
hook
import_url
- incoming_email_token
- rss_token
key
otp_attempt
- password
- password_confirmation
- private_token
- runners_token
- secret_token
sentry_dsn
variables
)
@@ -176,7 +169,7 @@ module Gitlab
next unless name.include?('namespace_project')
define_method(name.sub('namespace_project', 'project')) do |project, *args|
- send(name, project&.namespace, project, *args)
+ send(name, project&.namespace, project, *args) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index ca5b941aebf..d6c3c84851b 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -404,3 +404,15 @@
:why: https://github.com/mafintosh/thunky/blob/master/README.md#license
:versions: []
:when: 2017-08-07 05:56:09.907045000 Z
+- - :whitelist
+ - Unlicense
+ - :who: Nick Thomas <nick@gitlab.com>
+ :why: https://gitlab.com/gitlab-com/organization/issues/116
+ :versions: []
+ :when: 2017-09-01 17:17:51.996511844 Z
+- - :blacklist
+ - Facebook BSD+PATENTS
+ - :who: Nick Thomas <nick@gitlab.com>
+ :why: https://gitlab.com/gitlab-com/organization/issues/117
+ :versions: []
+ :when: 2017-09-04 12:59:51.150798717 Z
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index a9edde4d6e2..793ac0fdba9 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -76,6 +76,13 @@ production: &base
# default_can_create_group: false # default: true
# username_changing_enabled: false # default: true - User can change her username/namespace
+ ## Default theme ID
+ ## 1 - Indigo
+ ## 2 - Dark
+ ## 3 - Light
+ ## 4 - Blue
+ ## 5 - Green
+ # default_theme: 1 # default: 1
## Automatic issue closing
# If a commit message matches this regular expression, all issues referenced from the matched text will be closed.
@@ -281,9 +288,8 @@ production: &base
encryption: 'plain'
# Enables SSL certificate verification if encryption method is
- # "start_tls" or "simple_tls". (Defaults to false for backward-
- # compatibility)
- verify_certificates: false
+ # "start_tls" or "simple_tls". Defaults to true.
+ verify_certificates: true
# Specifies the path to a file containing a PEM-format CA certificate,
# e.g. if you need to use an internal CA.
@@ -381,9 +387,16 @@ production: &base
# showing GitLab's sign-in page (default: show the GitLab sign-in page)
# auto_sign_in_with_provider: saml
- # Sync user's email address from the specified Omniauth provider every time the user logs
- # in (default: nil). And consequently make this field read-only.
- # sync_email_from_provider: cas3
+ # Sync user's profile from the specified Omniauth providers every time the user logs in (default: empty).
+ # Define the allowed providers using an array, e.g. ["cas3", "saml", "twitter"],
+ # or as true/false to allow all providers or none.
+ # sync_profile_from_provider: []
+
+ # Select which info to sync from the providers above. (default: email).
+ # Define the synced profile info using an array. Available options are "name", "email" and "location"
+ # e.g. ["name", "email", "location"] or as true to sync all available.
+ # This consequently will make the selected attributes read-only.
+ # sync_profile_attributes: true
# CAUTION!
# This allows users to login without having a user account first. Define the allowed providers
@@ -517,7 +530,7 @@ production: &base
failure_count_threshold: 10 # number of failures before stopping attempts
failure_wait_time: 30 # Seconds after an access failure before allowing access again
failure_reset_time: 1800 # Time in seconds to expire failures
- storage_timeout: 5 # Time in seconds to wait before aborting a storage access attempt
+ storage_timeout: 30 # Time in seconds to wait before aborting a storage access attempt
## Backup settings
@@ -657,6 +670,9 @@ test:
default:
path: tmp/tests/repositories/
gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
+ failure_count_threshold: 999999
+ failure_wait_time: 0
+ storage_timeout: 30
broken:
path: tmp/tests/non-existent-repositories
gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
diff --git a/config/initializers/0_acts_as_taggable.rb b/config/initializers/0_acts_as_taggable.rb
index 54e9fcc31db..50dc47673ab 100644
--- a/config/initializers/0_acts_as_taggable.rb
+++ b/config/initializers/0_acts_as_taggable.rb
@@ -5,5 +5,5 @@ ActsAsTaggableOn.strict_case_match = true
ActsAsTaggableOn.tags_counter = false
# validate that counter cache is disabled
-raise "Counter cache is not disabled" if
+raise "Counter cache is not disabled" if
ActsAsTaggableOn::Tagging.reflections["tag"].options[:counter_cache]
diff --git a/config/initializers/0_inflections.rb b/config/initializers/0_inflections.rb
index f977104ff9d..1ad9ddca877 100644
--- a/config/initializers/0_inflections.rb
+++ b/config/initializers/0_inflections.rb
@@ -10,5 +10,10 @@
# end
#
ActiveSupport::Inflector.inflections do |inflect|
- inflect.uncountable %w(award_emoji project_statistics system_note_metadata)
+ inflect.uncountable %w(
+ award_emoji
+ project_statistics
+ system_note_metadata
+ project_auto_devops
+ )
end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index d804fb16167..5729206774e 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -1,3 +1,5 @@
+# rubocop:disable GitlabSecurity/PublicSend
+
require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible
class Settings < Settingslogic
@@ -71,7 +73,7 @@ class Settings < Settingslogic
# check that `current` (string or integer) is a contant in `modul`.
def verify_constant(modul, current, default)
- constant = modul.constants.find{ |name| modul.const_get(name) == current }
+ constant = modul.constants.find { |name| modul.const_get(name) == current }
value = constant.nil? ? default : modul.const_get(constant)
if current.is_a? String
value = modul.const_get(current.upcase) rescue default
@@ -137,6 +139,8 @@ if Settings.ldap['enabled'] || Rails.env.test?
end
Settings.ldap['servers'].each do |key, server|
+ server = Settingslogic.new(server)
+
server['label'] ||= 'LDAP'
server['timeout'] ||= 10.seconds
server['block_auto_created_users'] = false if server['block_auto_created_users'].nil?
@@ -151,18 +155,13 @@ if Settings.ldap['enabled'] || Rails.env.test?
server['encryption'] = 'simple_tls' if server['encryption'] == 'ssl'
server['encryption'] = 'start_tls' if server['encryption'] == 'tls'
- # Certificates are not verified for backwards compatibility.
- # This default should be flipped to true in 9.5.
- if server['verify_certificates'].nil?
- server['verify_certificates'] = false
-
- message = <<-MSG.strip_heredoc
- LDAP SSL certificate verification is disabled for backwards-compatibility.
- Please add the "verify_certificates" option to gitlab.yml for each LDAP
- server. Certificate verification will be enabled by default in GitLab 9.5.
- MSG
- Rails.logger.warn(message)
- end
+ # Certificate verification was added in 9.4.2, and defaulted to false for
+ # backwards-compatibility.
+ #
+ # Since GitLab 10.0, verify_certificates defaults to true for security.
+ server['verify_certificates'] = true if server['verify_certificates'].nil?
+
+ Settings.ldap['servers'][key] = server
end
end
@@ -174,7 +173,20 @@ Settings.omniauth['external_providers'] = [] if Settings.omniauth['external_prov
Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block_auto_created_users'].nil?
Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil?
Settings.omniauth['auto_link_saml_user'] = false if Settings.omniauth['auto_link_saml_user'].nil?
-Settings.omniauth['sync_email_from_provider'] ||= nil
+
+Settings.omniauth['sync_profile_from_provider'] = false if Settings.omniauth['sync_profile_from_provider'].nil?
+Settings.omniauth['sync_profile_attributes'] = ['email'] if Settings.omniauth['sync_profile_attributes'].nil?
+
+# Handle backwards compatibility with merge request 11268
+if Settings.omniauth['sync_email_from_provider']
+ if Settings.omniauth['sync_profile_from_provider'].is_a?(Array)
+ Settings.omniauth['sync_profile_from_provider'] |= [Settings.omniauth['sync_email_from_provider']]
+ elsif !Settings.omniauth['sync_profile_from_provider']
+ Settings.omniauth['sync_profile_from_provider'] = [Settings.omniauth['sync_email_from_provider']]
+ end
+
+ Settings.omniauth['sync_profile_attributes'] |= ['email'] unless Settings.omniauth['sync_profile_attributes'] == true
+end
Settings.omniauth['providers'] ||= []
Settings.omniauth['cas3'] ||= Settingslogic.new({})
@@ -220,6 +232,7 @@ Settings['gitlab'] ||= Settingslogic.new({})
Settings.gitlab['default_projects_limit'] ||= 100000
Settings.gitlab['default_branch_protection'] ||= 2
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
+Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?
@@ -440,7 +453,9 @@ unless Settings.repositories.storages['default']
Settings.repositories.storages['default']['path'] ||= Settings.gitlab['user_home'] + '/repositories/'
end
-Settings.repositories.storages.values.each do |storage|
+Settings.repositories.storages.each do |key, storage|
+ storage = Settingslogic.new(storage)
+
# Expand relative paths
storage['path'] = Settings.absolute(storage['path'])
# Set failure defaults
@@ -454,6 +469,8 @@ Settings.repositories.storages.values.each do |storage|
storage['failure_reset_time'] = storage['failure_reset_time'].to_i
# We might want to have a timeout shorter than 1 second.
storage['storage_timeout'] = storage['storage_timeout'].to_f
+
+ Settings.repositories.storages[key] = storage
end
#
diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb
index 92ce4dd03cd..f8e67ce04c9 100644
--- a/config/initializers/6_validations.rb
+++ b/config/initializers/6_validations.rb
@@ -37,12 +37,12 @@ def validate_storages_config
storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example")
end
- %w(failure_count_threshold failure_wait_time failure_reset_time storage_timeout).each do |setting|
+ %w(failure_count_threshold failure_reset_time storage_timeout).each do |setting|
# Falling back to the defaults is fine!
next if repository_storage[setting].nil?
unless repository_storage[setting].to_f > 0
- storage_validation_error("#{setting}, for storage `#{name}` needs to be greater than 0")
+ storage_validation_error("`#{setting}` for storage `#{name}` needs to be greater than 0")
end
end
end
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 54c797e0714..31839297523 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -1,4 +1,5 @@
require 'prometheus/client'
+require 'prometheus/client/support/unicorn'
Prometheus::Client.configure do |config|
config.logger = Rails.logger
@@ -9,6 +10,8 @@ Prometheus::Client.configure do |config|
if Rails.env.development? || Rails.env.test?
config.multiprocess_files_dir ||= Rails.root.join('tmp/prometheus_multiproc_dir')
end
+
+ config.pid_provider = Prometheus::Client::Support::Unicorn.method(:worker_pid_provider)
end
Sidekiq.configure_server do |config|
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index 2aeb94d47cd..e1a59d8c152 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -114,11 +114,12 @@ def instrument_classes(instrumentation)
# This is a Rails scope so we have to instrument it manually.
instrumentation.instrument_method(Project, :visible_to_user)
- # Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/34509
- instrumentation.instrument_method(MarkupHelper, :link_to_gfm)
-
# Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/30224#note_32306159
instrumentation.instrument_instance_method(MergeRequestDiff, :load_commits)
+
+ # Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/36061
+ instrumentation.instrument_instance_method(MergeRequest, :ensure_ref_fetched)
+ instrumentation.instrument_instance_method(MergeRequest, :fetch_ref)
end
# rubocop:enable Metrics/AbcSize
diff --git a/config/initializers/active_record_array_type_casting.rb b/config/initializers/active_record_array_type_casting.rb
new file mode 100644
index 00000000000..d94d592add6
--- /dev/null
+++ b/config/initializers/active_record_array_type_casting.rb
@@ -0,0 +1,20 @@
+module ActiveRecord
+ class PredicateBuilder
+ class ArrayHandler
+ module TypeCasting
+ def call(attribute, value)
+ # This is necessary because by default ActiveRecord does not respect
+ # custom type definitions (like our `ShaAttribute`) when providing an
+ # array in `where`, like in `where(commit_sha: [sha1, sha2, sha3])`.
+ model = attribute.relation&.engine
+ type = model.user_provided_columns[attribute.name] if model
+ value = value.map { |value| type.type_cast_for_database(value) } if type
+
+ super(attribute, value)
+ end
+ end
+
+ prepend TypeCasting
+ end
+ end
+end
diff --git a/config/initializers/active_record_locking.rb b/config/initializers/active_record_locking.rb
index 9266ff0f615..150aaa2a8c2 100644
--- a/config/initializers/active_record_locking.rb
+++ b/config/initializers/active_record_locking.rb
@@ -18,7 +18,7 @@ module ActiveRecord
lock_col = self.class.locking_column
- previous_lock_value = send(lock_col).to_i
+ previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend
# This line is added as a patch
previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
@@ -48,7 +48,7 @@ module ActiveRecord
# If something went wrong, revert the version.
rescue Exception
- send(lock_col + '=', previous_lock_value)
+ send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend
raise
end
end
diff --git a/config/initializers/active_record_mysql_timestamp.rb b/config/initializers/active_record_mysql_timestamp.rb
new file mode 100644
index 00000000000..af74c4ff6fb
--- /dev/null
+++ b/config/initializers/active_record_mysql_timestamp.rb
@@ -0,0 +1,30 @@
+# Make sure that MySQL won't try to use CURRENT_TIMESTAMP when the timestamp
+# column is NOT NULL. See https://gitlab.com/gitlab-org/gitlab-ce/issues/36405
+# And also: https://bugs.mysql.com/bug.php?id=75098
+# This patch was based on:
+# https://github.com/rails/rails/blob/15ef55efb591e5379486ccf53dd3e13f416564f6/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb#L34-L36
+
+if Gitlab::Database.mysql?
+ require 'active_record/connection_adapters/abstract/schema_creation'
+
+ module MySQLTimestampFix
+ def add_column_options!(sql, options)
+ # By default, TIMESTAMP columns are NOT NULL, cannot contain NULL values,
+ # and assigning NULL assigns the current timestamp. To permit a TIMESTAMP
+ # column to contain NULL, explicitly declare it with the NULL attribute.
+ # See http://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html
+ if sql.end_with?('timestamp') && !options[:primary_key]
+ if options[:null] != false
+ sql << ' NULL'
+ elsif options[:column].default.nil?
+ sql << ' DEFAULT 0'
+ end
+ end
+
+ super
+ end
+ end
+
+ ActiveRecord::ConnectionAdapters::AbstractAdapter::SchemaCreation
+ .prepend(MySQLTimestampFix)
+end
diff --git a/config/initializers/fast_gettext.rb b/config/initializers/fast_gettext.rb
index eb589ecdb52..fd0167aa476 100644
--- a/config/initializers/fast_gettext.rb
+++ b/config/initializers/fast_gettext.rb
@@ -1,4 +1,7 @@
-FastGettext.add_text_domain 'gitlab', path: File.join(Rails.root, 'locale'), type: :po
+FastGettext.add_text_domain 'gitlab',
+ path: File.join(Rails.root, 'locale'),
+ type: :po,
+ ignore_fuzzy: true
FastGettext.default_text_domain = 'gitlab'
FastGettext.default_available_locales = Gitlab::I18n.available_locales
FastGettext.default_locale = :en
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 56c279ffcf4..fddb018e948 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -6,12 +6,6 @@ if Gitlab::LDAP::Config.enabled?
const_set(server['provider_class'], Class.new(LDAP))
end
end
-
- OmniauthCallbacksController.class_eval do
- Gitlab::LDAP::Config.available_servers.each do |server|
- alias_method server['provider_name'], :ldap
- end
- end
end
OmniAuth.config.full_host = Settings.gitlab['base_url']
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 6b0cff75653..62d0967009a 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -1,19 +1,18 @@
# Be sure to restart your server when you modify this file.
require 'gitlab/current_settings'
-include Gitlab::CurrentSettings
if Rails.env.production?
# allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done
begin
- sentry_enabled = current_application_settings.sentry_enabled
+ sentry_enabled = Gitlab::CurrentSettings.current_application_settings.sentry_enabled
rescue
sentry_enabled = false
end
if sentry_enabled
Raven.configure do |config|
- config.dsn = current_application_settings.sentry_dsn
+ config.dsn = Gitlab::CurrentSettings.current_application_settings.sentry_dsn
config.release = Gitlab::REVISION
# Sanitize fields based on those sanitized from Rails.
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index e8213ac8ba4..f2fde1e0048 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -1,11 +1,10 @@
# Be sure to restart your server when you modify this file.
require 'gitlab/current_settings'
-include Gitlab::CurrentSettings
# allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done
begin
- Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay || 10080
+ Settings.gitlab['session_expire_delay'] = Gitlab::CurrentSettings.current_application_settings.session_expire_delay || 10080
rescue
Settings.gitlab['session_expire_delay'] ||= 10080
end
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index 9ed96ddb0b4..943e01f1496 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -1,15 +1,15 @@
app = Rails.application
if app.config.serve_static_files
- # The `ActionDispatch::Static` middleware intercepts requests for static files
- # by checking if they exist in the `/public` directory.
+ # The `ActionDispatch::Static` middleware intercepts requests for static files
+ # by checking if they exist in the `/public` directory.
# We're replacing it with our `Gitlab::Middleware::Static` that does the same,
# except ignoring `/uploads`, letting those go through to the GitLab Rails app.
app.config.middleware.swap(
- ActionDispatch::Static,
- Gitlab::Middleware::Static,
- app.paths["public"].first,
+ ActionDispatch::Static,
+ Gitlab::Middleware::Static,
+ app.paths["public"].first,
app.config.static_cache_control
)
diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb
index fc4f02453d7..0c32528311e 100644
--- a/config/initializers/trusted_proxies.rb
+++ b/config/initializers/trusted_proxies.rb
@@ -2,7 +2,7 @@
# as the ActionDispatch::Request object. This is necessary for libraries
# like rack_attack where they don't use ActionDispatch, and we want them
# to block/throttle requests on private networks.
-# Rack Attack specific issue: https://github.com/kickstarter/rack-attack/issues/145
+# Rack Attack specific issue: https://github.com/kickstarter/rack-attack/issues/145
module Rack
class Request
def trusted_proxy?(ip)
diff --git a/config/initializers/workhorse_multipart.rb b/config/initializers/workhorse_multipart.rb
index 064e5964f09..4196e3a8f61 100644
--- a/config/initializers/workhorse_multipart.rb
+++ b/config/initializers/workhorse_multipart.rb
@@ -10,10 +10,8 @@ end
#
module Gitlab
module StrongParameterScalars
- GITLAB_PERMITTED_SCALAR_TYPES = [::UploadedFile].freeze
-
def permitted_scalar?(value)
- super || GITLAB_PERMITTED_SCALAR_TYPES.any? { |type| value.is_a?(type) }
+ super || value.is_a?(::UploadedFile)
end
end
end
diff --git a/config/karma.config.js b/config/karma.config.js
index 2f571978e08..e459f5cdac3 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -8,6 +8,7 @@ if (webpackConfig.plugins) {
webpackConfig.plugins = webpackConfig.plugins.filter(function (plugin) {
return !(
plugin instanceof webpack.optimize.CommonsChunkPlugin ||
+ plugin instanceof webpack.optimize.ModuleConcatenationPlugin ||
plugin instanceof webpack.DefinePlugin
);
});
diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml
index 5eb01d62924..0642a0b2fe9 100644
--- a/config/prometheus/additional_metrics.yml
+++ b/config/prometheus/additional_metrics.yml
@@ -1,3 +1,33 @@
+- group: Response metrics (NGINX Ingress)
+ priority: 10
+ metrics:
+ - title: "Throughput"
+ y_label: "Requests / Sec"
+ required_metrics:
+ - nginx_upstream_requests_total
+ weight: 1
+ queries:
+ - query_range: 'sum(rate(nginx_upstream_requests_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))'
+ label: Total
+ unit: req / sec
+ - title: "Latency"
+ y_label: "Latency (ms)"
+ required_metrics:
+ - nginx_upstream_response_msecs_avg
+ weight: 1
+ queries:
+ - query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"})'
+ label: Average
+ unit: ms
+ - title: "HTTP Error Rate"
+ y_label: "HTTP 500 Errors / Sec"
+ required_metrics:
+ - nginx_upstream_responses_total
+ weight: 1
+ queries:
+ - query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))'
+ label: HTTP Errors
+ unit: "errors / sec"
- group: Response metrics (HA Proxy)
priority: 10
metrics:
@@ -68,18 +98,18 @@
- nginx_upstream_response_msecs_avg
weight: 1
queries:
- - query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) * 1000'
+ - query_range: 'avg(nginx_upstream_response_msecs_avg{%{environment_filter}})'
label: Upstream
unit: ms
- title: "HTTP Error Rate"
- y_label: "Error Rate (%)"
+ y_label: "HTTP 500 Errors / Sec"
required_metrics:
- nginx_responses_total
weight: 1
queries:
- - query_range: 'sum(rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) / sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m]))'
+ - query_range: 'sum(rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m]))'
label: HTTP Errors
- unit: "%"
+ unit: "errors / sec"
- group: System metrics (Kubernetes)
priority: 5
metrics:
diff --git a/config/routes.rb b/config/routes.rb
index 4fd6cb5d439..5683725c8a2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -27,6 +27,7 @@ Rails.application.routes.draw do
get '/autocomplete/users' => 'autocomplete#users'
get '/autocomplete/users/:id' => 'autocomplete#user'
get '/autocomplete/projects' => 'autocomplete#projects'
+ get '/autocomplete/award_emojis' => 'autocomplete#award_emojis'
# Search
get 'search' => 'search#show'
@@ -73,6 +74,19 @@ Rails.application.routes.draw do
# Notification settings
resources :notification_settings, only: [:create, :update]
+ # Boards resources shared between group and projects
+ resources :boards do
+ resources :lists, module: :boards, only: [:index, :create, :update, :destroy] do
+ collection do
+ post :generate
+ end
+
+ resources :issues, only: [:index, :create, :update]
+ end
+
+ resources :issues, module: :boards, only: [:index, :update]
+ end
+
draw :import
draw :uploads
draw :explore
diff --git a/config/routes/ci.rb b/config/routes/ci.rb
index 8d23aa8fbf6..cbd4c2db852 100644
--- a/config/routes/ci.rb
+++ b/config/routes/ci.rb
@@ -1,8 +1,4 @@
namespace :ci do
- # CI API
- Ci::API::API.logger Rails.logger
- mount Ci::API::API => '/api'
-
resource :lint, only: [:show, :create]
root to: redirect('/')
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 06928c7b9ce..b36d13888cd 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -303,11 +303,13 @@ constraints(ProjectUrlConstrainer.new) do
member do
post :toggle_subscription
post :mark_as_spam
+ post :move
get :referenced_merge_requests
get :related_branches
get :can_create_branch
get :realtime_changes
post :create_merge_request
+ get :discussions, format: :json
end
collection do
post :bulk_update
@@ -341,19 +343,7 @@ constraints(ProjectUrlConstrainer.new) do
get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes'
- resources :boards, only: [:index, :show] do
- scope module: :boards do
- resources :issues, only: [:index, :update]
-
- resources :lists, only: [:index, :create, :update, :destroy] do
- collection do
- post :generate
- end
-
- resources :issues, only: [:index, :create]
- end
- end
- end
+ resources :boards, only: [:index, :show, :create, :update, :destroy]
resources :todos, only: [:create]
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index 2ba16035ece..9ffdebbcff1 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -3,6 +3,9 @@
resource :repository, only: [:create] do
member do
get ':ref/archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex, ref: /.+/ }, action: 'archive', as: 'archive'
+
+ # deprecated since GitLab 9.5
+ get 'archive', constraints: { format: Gitlab::PathRegex.archive_formats_regex }, as: 'archive_alternative'
end
end
diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb
index e9c9aa8b2f9..d7bca8310e4 100644
--- a/config/routes/uploads.rb
+++ b/config/routes/uploads.rb
@@ -5,12 +5,12 @@ scope path: :uploads do
constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
# show uploads for models, snippets (notes) available for now
- get 'system/:model/:id/:secret/:filename',
+ get '-/system/:model/:id/:secret/:filename',
to: 'uploads#show',
constraints: { model: /personal_snippet/, id: /\d+/, filename: /[^\/]+/ }
# show temporary uploads
- get 'system/temp/:secret/:filename',
+ get '-/system/temp/:secret/:filename',
to: 'uploads#show',
constraints: { filename: /[^\/]+/ }
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 83abc83c9f0..24c001362c6 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -27,6 +27,10 @@
- [new_merge_request, 2]
- [build, 2]
- [pipeline, 2]
+ - [pipeline_processing, 5]
+ - [pipeline_default, 3]
+ - [pipeline_cache, 3]
+ - [pipeline_hooks, 2]
- [gitlab_shell, 2]
- [email_receiver, 2]
- [emails_on_push, 2]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 8e1b80cd39f..6b0cd023291 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -30,7 +30,7 @@ var config = {
blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js',
common: './commons/index.js',
- common_vue: ['vue', './vue_shared/common_vue.js'],
+ common_vue: './vue_shared/vue_resource_interceptor.js',
common_d3: ['d3'],
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js',
@@ -55,6 +55,7 @@ var config = {
monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js',
+ notes: './notes/index.js',
pdf_viewer: './blob/pdf_viewer.js',
pipelines: './pipelines/pipelines_bundle.js',
pipelines_charts: './pipelines/pipelines_charts.js',
@@ -76,7 +77,6 @@ var config = {
terminal: './terminal/terminal_bundle.js',
u2f: ['vendor/u2f'],
ui_development_kit: './ui_development_kit.js',
- users: './users/index.js',
raven: './raven/index.js',
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
@@ -171,7 +171,7 @@ var config = {
if (chunk.name) {
return chunk.name;
}
- return chunk.modules.map((m) => {
+ return chunk.mapModules((m) => {
var chunkPath = m.request.split('!').pop();
return path.relative(m.context, chunkPath);
}).join('_');
@@ -195,6 +195,7 @@ var config = {
'merge_conflicts',
'monitoring',
'notebook_viewer',
+ 'notes',
'pdf_viewer',
'pipelines',
'pipelines_details',
@@ -223,6 +224,9 @@ var config = {
names: ['main', 'locale', 'common', 'webpack_runtime'],
}),
+ // enable scope hoisting
+ new webpack.optimize.ModuleConcatenationPlugin(),
+
// copy pre-compiled vendor libraries verbatim
new CopyWebpackPlugin([
{
@@ -274,14 +278,9 @@ if (IS_PRODUCTION) {
})
);
- // zopfli requires a lot of compute time and is disabled in CI
+ // compression can require a lot of compute time and is disabled in CI
if (!NO_COMPRESSION) {
- // gracefully fall back to gzip if `node-zopfli` is unavailable (e.g. in CentOS 6)
- try {
- config.plugins.push(new CompressionPlugin({ algorithm: 'zopfli' }));
- } catch(err) {
- config.plugins.push(new CompressionPlugin({ algorithm: 'gzip' }));
- }
+ config.plugins.push(new CompressionPlugin());
}
}
diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb
index c304e0706dc..30244ee4431 100644
--- a/db/fixtures/development/10_merge_requests.rb
+++ b/db/fixtures/development/10_merge_requests.rb
@@ -28,6 +28,8 @@ Gitlab::Seeder.quiet do
project = Project.find_by_full_path('gitlab-org/gitlab-test')
+ next if project.empty_repo? # We don't have repository on CI
+
params = {
source_branch: 'feature',
target_branch: 'master',
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index 7c1d758dada..383782112a8 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -15,7 +15,7 @@ class Gitlab::Seeder::CycleAnalytics
# to disable the `pre_receive` hook in order to remove this
# dependency on the GitLab API.
def stub_git_pre_receive!
- GitHooksService.class_eval do
+ Gitlab::Git::HooksService.class_eval do
def run_hook(name)
[true, '']
end
diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb
index f5d5d834307..ca1b054600c 100644
--- a/db/migrate/20140502125220_migrate_repo_size.rb
+++ b/db/migrate/20140502125220_migrate_repo_size.rb
@@ -11,7 +11,7 @@ class MigrateRepoSize < ActiveRecord::Migration
path = File.join(namespace_path, project['project_path'] + '.git')
begin
- repo = Gitlab::Git::Repository.new('default', path)
+ repo = Gitlab::Git::Repository.new('default', path, '')
if repo.empty?
print '-'
else
diff --git a/db/migrate/20160713205315_add_domain_blacklist_to_application_settings.rb b/db/migrate/20160713205315_add_domain_blacklist_to_application_settings.rb
index ecdd1bd7e5e..f64dfa7675f 100644
--- a/db/migrate/20160713205315_add_domain_blacklist_to_application_settings.rb
+++ b/db/migrate/20160713205315_add_domain_blacklist_to_application_settings.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable Migration/SaferBooleanColumn
class AddDomainBlacklistToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160817133006_add_koding_to_application_settings.rb b/db/migrate/20160817133006_add_koding_to_application_settings.rb
index 915d3d78e40..46120652d8e 100644
--- a/db/migrate/20160817133006_add_koding_to_application_settings.rb
+++ b/db/migrate/20160817133006_add_koding_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/SaferBooleanColumn
class AddKodingToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161017125927_add_unique_index_to_labels.rb b/db/migrate/20161017125927_add_unique_index_to_labels.rb
index b8f6a803a0a..fcdd79d3b02 100644
--- a/db/migrate/20161017125927_add_unique_index_to_labels.rb
+++ b/db/migrate/20161017125927_add_unique_index_to_labels.rb
@@ -10,7 +10,7 @@ class AddUniqueIndexToLabels < ActiveRecord::Migration
def up
select_all('SELECT title, project_id, COUNT(id) as cnt FROM labels GROUP BY project_id, title HAVING COUNT(id) > 1').each do |label|
label_title = quote_string(label['title'])
- duplicated_ids = select_all("SELECT id FROM labels WHERE project_id = #{label['project_id']} AND title = '#{label_title}' ORDER BY id ASC").map{ |label| label['id'] }
+ duplicated_ids = select_all("SELECT id FROM labels WHERE project_id = #{label['project_id']} AND title = '#{label_title}' ORDER BY id ASC").map { |label| label['id'] }
label_id = duplicated_ids.first
duplicated_ids.delete(label_id)
diff --git a/db/migrate/20161020075830_default_request_access_projects.rb b/db/migrate/20161020075830_default_request_access_projects.rb
index cb790291b24..a3a53350e8d 100644
--- a/db/migrate/20161020075830_default_request_access_projects.rb
+++ b/db/migrate/20161020075830_default_request_access_projects.rb
@@ -1,7 +1,7 @@
class DefaultRequestAccessProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
-
+
def up
change_column_default :projects, :request_access_enabled, false
end
diff --git a/db/migrate/20161020180657_add_minimum_key_length_to_application_settings.rb b/db/migrate/20161020180657_add_minimum_key_length_to_application_settings.rb
new file mode 100644
index 00000000000..5b6079002c0
--- /dev/null
+++ b/db/migrate/20161020180657_add_minimum_key_length_to_application_settings.rb
@@ -0,0 +1,29 @@
+class AddMinimumKeyLengthToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # A key restriction has these possible states:
+ #
+ # * -1 means "this key type is completely disabled"
+ # * 0 means "all keys of this type are valid"
+ # * > 0 means "keys must have at least this many bits to be valid"
+ #
+ # The default is 0, for backward compatibility
+ add_column_with_default :application_settings, :rsa_key_restriction, :integer, default: 0
+ add_column_with_default :application_settings, :dsa_key_restriction, :integer, default: 0
+ add_column_with_default :application_settings, :ecdsa_key_restriction, :integer, default: 0
+ add_column_with_default :application_settings, :ed25519_key_restriction, :integer, default: 0
+ end
+
+ def down
+ remove_column :application_settings, :rsa_key_restriction
+ remove_column :application_settings, :dsa_key_restriction
+ remove_column :application_settings, :ecdsa_key_restriction
+ remove_column :application_settings, :ed25519_key_restriction
+ end
+end
diff --git a/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb b/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb
index e644a174964..522437b92b4 100644
--- a/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb
+++ b/db/migrate/20161103191444_add_sidekiq_throttling_to_application_settings.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable Migration/SaferBooleanColumn
class AddSidekiqThrottlingToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb b/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb
index 1c59241d0fe..38f5781745b 100644
--- a/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb
+++ b/db/migrate/20161128161412_add_html_emails_enabled_to_application_settings.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable Migration/SaferBooleanColumn
class AddHtmlEmailsEnabledToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb b/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb
index 3677f978cc2..7f56ecf4c9e 100644
--- a/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb
+++ b/db/migrate/20161206003819_add_plant_uml_enabled_to_application_settings.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable Migration/SaferBooleanColumn
class AddPlantUmlEnabledToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170316163800_rename_system_namespaces.rb b/db/migrate/20170316163800_rename_system_namespaces.rb
deleted file mode 100644
index 9e9fb5ac225..00000000000
--- a/db/migrate/20170316163800_rename_system_namespaces.rb
+++ /dev/null
@@ -1,231 +0,0 @@
-# See http://doc.gitlab.com/ce/development/migration_style_guide.html
-# for more information on how to write migrations for GitLab.
-class RenameSystemNamespaces < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
- include Gitlab::ShellAdapter
- disable_ddl_transaction!
-
- class User < ActiveRecord::Base
- self.table_name = 'users'
- end
-
- class Namespace < ActiveRecord::Base
- self.table_name = 'namespaces'
- belongs_to :parent, class_name: 'RenameSystemNamespaces::Namespace'
- has_one :route, as: :source
- has_many :children, class_name: 'RenameSystemNamespaces::Namespace', foreign_key: :parent_id
- belongs_to :owner, class_name: 'RenameSystemNamespaces::User'
-
- # Overridden to have the correct `source_type` for the `route` relation
- def self.name
- 'Namespace'
- end
-
- def full_path
- if route && route.path.present?
- @full_path ||= route.path
- else
- update_route if persisted?
-
- build_full_path
- end
- end
-
- def build_full_path
- if parent && path
- parent.full_path + '/' + path
- else
- path
- end
- end
-
- def update_route
- prepare_route
- route.save
- end
-
- def prepare_route
- route || build_route(source: self)
- route.path = build_full_path
- route.name = build_full_name
- @full_path = nil
- @full_name = nil
- end
-
- def build_full_name
- if parent && name
- parent.human_name + ' / ' + name
- else
- name
- end
- end
-
- def human_name
- owner&.name
- end
- end
-
- class Route < ActiveRecord::Base
- self.table_name = 'routes'
- belongs_to :source, polymorphic: true
- end
-
- class Project < ActiveRecord::Base
- self.table_name = 'projects'
-
- def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage]['path']
- end
- end
-
- DOWNTIME = false
-
- def up
- return unless system_namespace
-
- old_path = system_namespace.path
- old_full_path = system_namespace.full_path
- # Only remove the last occurrence of the path name to get the parent namespace path
- namespace_path = remove_last_occurrence(old_full_path, old_path)
- new_path = rename_path(namespace_path, old_path)
- new_full_path = join_namespace_path(namespace_path, new_path)
-
- Namespace.where(id: system_namespace).update_all(path: new_path) # skips callbacks & validations
-
- replace_statement = replace_sql(Route.arel_table[:path], old_full_path, new_full_path)
- route_matches = [old_full_path, "#{old_full_path}/%"]
-
- update_column_in_batches(:routes, :path, replace_statement) do |table, query|
- query.where(Route.arel_table[:path].matches_any(route_matches))
- end
-
- clear_cache_for_namespace(system_namespace)
-
- # tasks here are based on `Namespace#move_dir`
- move_repositories(system_namespace, old_full_path, new_full_path)
- move_namespace_folders(uploads_dir, old_full_path, new_full_path) if file_storage?
- move_namespace_folders(pages_dir, old_full_path, new_full_path)
- end
-
- def down
- # nothing to do
- end
-
- def remove_last_occurrence(string, pattern)
- string.reverse.sub(pattern.reverse, "").reverse
- end
-
- def move_namespace_folders(directory, old_relative_path, new_relative_path)
- old_path = File.join(directory, old_relative_path)
- return unless File.directory?(old_path)
-
- new_path = File.join(directory, new_relative_path)
- FileUtils.mv(old_path, new_path)
- end
-
- def move_repositories(namespace, old_full_path, new_full_path)
- repo_paths_for_namespace(namespace).each do |repository_storage_path|
- # Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage_path, old_full_path)
-
- unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path)
- say "Exception moving path #{repository_storage_path} from #{old_full_path} to #{new_full_path}"
- end
- end
- end
-
- def rename_path(namespace_path, path_was)
- counter = 0
- path = "#{path_was}#{counter}"
-
- while route_exists?(join_namespace_path(namespace_path, path))
- counter += 1
- path = "#{path_was}#{counter}"
- end
-
- path
- end
-
- def route_exists?(full_path)
- Route.where(Route.arel_table[:path].matches(full_path)).any?
- end
-
- def join_namespace_path(namespace_path, path)
- if namespace_path.present?
- File.join(namespace_path, path)
- else
- path
- end
- end
-
- def system_namespace
- @system_namespace ||= Namespace.where(parent_id: nil)
- .where(arel_table[:path].matches(system_namespace_path))
- .first
- end
-
- def system_namespace_path
- "system"
- end
-
- def clear_cache_for_namespace(namespace)
- project_ids = projects_for_namespace(namespace).pluck(:id)
-
- update_column_in_batches(:projects, :description_html, nil) do |table, query|
- query.where(table[:id].in(project_ids))
- end
-
- update_column_in_batches(:issues, :description_html, nil) do |table, query|
- query.where(table[:project_id].in(project_ids))
- end
-
- update_column_in_batches(:merge_requests, :description_html, nil) do |table, query|
- query.where(table[:target_project_id].in(project_ids))
- end
-
- update_column_in_batches(:notes, :note_html, nil) do |table, query|
- query.where(table[:project_id].in(project_ids))
- end
-
- update_column_in_batches(:milestones, :description_html, nil) do |table, query|
- query.where(table[:project_id].in(project_ids))
- end
- end
-
- def projects_for_namespace(namespace)
- namespace_ids = child_ids_for_parent(namespace, ids: [namespace.id])
- namespace_or_children = Project.arel_table[:namespace_id].in(namespace_ids)
- Project.unscoped.where(namespace_or_children)
- end
-
- # This won't scale to huge trees, but it should do for a handful of namespaces
- # called `system`.
- def child_ids_for_parent(namespace, ids: [])
- namespace.children.each do |child|
- ids << child.id
- child_ids_for_parent(child, ids: ids) if child.children.any?
- end
- ids
- end
-
- def repo_paths_for_namespace(namespace)
- projects_for_namespace(namespace).distinct
- .select(:repository_storage).map(&:repository_storage_path)
- end
-
- def uploads_dir
- File.join(Rails.root, "public", "uploads")
- end
-
- def pages_dir
- Settings.pages.path
- end
-
- def file_storage?
- CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File
- end
-
- def arel_table
- Namespace.arel_table
- end
-end
diff --git a/db/migrate/20170316163845_move_uploads_to_system_dir.rb b/db/migrate/20170316163845_move_uploads_to_system_dir.rb
index 564ee10b5ab..cfcb909ddaf 100644
--- a/db/migrate/20170316163845_move_uploads_to_system_dir.rb
+++ b/db/migrate/20170316163845_move_uploads_to_system_dir.rb
@@ -54,6 +54,6 @@ class MoveUploadsToSystemDir < ActiveRecord::Migration
end
def new_upload_dir
- File.join(base_directory, "public", "uploads", "system")
+ File.join(base_directory, "public", "uploads", "-", "system")
end
end
diff --git a/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb b/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb
index 5e8b667b86d..d358020d182 100644
--- a/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb
+++ b/db/migrate/20170602154736_add_help_page_hide_commercial_content_to_application_settings.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Migration/SaferBooleanColumn
class AddHelpPageHideCommercialContentToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170608152747_prepare_events_table_for_push_events_migration.rb b/db/migrate/20170608152747_prepare_events_table_for_push_events_migration.rb
new file mode 100644
index 00000000000..f4f03bbabaf
--- /dev/null
+++ b/db/migrate/20170608152747_prepare_events_table_for_push_events_migration.rb
@@ -0,0 +1,51 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PrepareEventsTableForPushEventsMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # The order of these columns is deliberate and results in the following
+ # columns and sizes:
+ #
+ # * id (4 bytes)
+ # * project_id (4 bytes)
+ # * author_id (4 bytes)
+ # * target_id (4 bytes)
+ # * created_at (8 bytes)
+ # * updated_at (8 bytes)
+ # * action (2 bytes)
+ # * target_type (variable)
+ #
+ # Unfortunately we can't make the "id" column a bigint/bigserial as Rails 4
+ # does not support this properly.
+ create_table :events_for_migration do |t|
+ t.references :project,
+ index: true,
+ foreign_key: { on_delete: :cascade }
+
+ t.integer :author_id, index: true, null: false
+ t.integer :target_id
+
+ t.timestamps_with_timezone null: false
+
+ t.integer :action, null: false, limit: 2, index: true
+ t.string :target_type
+
+ t.index %i[target_type target_id]
+ end
+
+ # t.references doesn't like it when the column name doesn't make the table
+ # name so we have to add the foreign key separately.
+ add_concurrent_foreign_key(:events_for_migration, :users, column: :author_id)
+ end
+
+ def down
+ drop_table :events_for_migration
+ end
+end
diff --git a/db/migrate/20170608152748_create_push_event_payloads_tables.rb b/db/migrate/20170608152748_create_push_event_payloads_tables.rb
new file mode 100644
index 00000000000..6c55ad1f2f7
--- /dev/null
+++ b/db/migrate/20170608152748_create_push_event_payloads_tables.rb
@@ -0,0 +1,46 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreatePushEventPayloadsTables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :push_event_payloads, id: false do |t|
+ t.bigint :commit_count, null: false
+
+ t.integer :event_id, null: false
+ t.integer :action, null: false, limit: 2
+ t.integer :ref_type, null: false, limit: 2
+
+ t.binary :commit_from
+ t.binary :commit_to
+
+ t.text :ref
+ t.string :commit_title, limit: 70
+
+ t.index :event_id, unique: true
+ end
+
+ # We're adding a foreign key to the _shadow_ table, and this is deliberate.
+ # By using the shadow table we don't have to recreate/revalidate this
+ # foreign key after swapping the "events_for_migration" and "events" tables.
+ #
+ # The "events_for_migration" table has a foreign key to "projects.id"
+ # ensuring that project removals also remove events from the shadow table
+ # (and thus also from this table).
+ add_concurrent_foreign_key(
+ :push_event_payloads,
+ :events_for_migration,
+ column: :event_id
+ )
+ end
+
+ def down
+ drop_table :push_event_payloads
+ end
+end
diff --git a/db/migrate/20170711145320_add_status_to_ci_stages.rb b/db/migrate/20170711145320_add_status_to_ci_stages.rb
new file mode 100644
index 00000000000..d497a61a959
--- /dev/null
+++ b/db/migrate/20170711145320_add_status_to_ci_stages.rb
@@ -0,0 +1,9 @@
+class AddStatusToCiStages < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_stages, :status, :integer
+ end
+end
diff --git a/db/migrate/20170717074009_move_system_upload_folder.rb b/db/migrate/20170717074009_move_system_upload_folder.rb
index cce31794115..d3caa53a7a4 100644
--- a/db/migrate/20170717074009_move_system_upload_folder.rb
+++ b/db/migrate/20170717074009_move_system_upload_folder.rb
@@ -15,6 +15,11 @@ class MoveSystemUploadFolder < ActiveRecord::Migration
return
end
+ if File.directory?(new_directory)
+ say "#{new_directory} already exists. No need to redo the move."
+ return
+ end
+
FileUtils.mkdir_p(File.join(base_directory, '-'))
say "Moving #{old_directory} -> #{new_directory}"
@@ -33,6 +38,11 @@ class MoveSystemUploadFolder < ActiveRecord::Migration
return
end
+ if !File.symlink?(old_directory) && File.directory?(old_directory)
+ say "#{old_directory} already exists and is not a symlink, no need to revert."
+ return
+ end
+
if File.symlink?(old_directory)
say "Removing #{old_directory} -> #{new_directory} symlink"
FileUtils.rm(old_directory)
diff --git a/db/migrate/20170720111708_add_lock_version_to_ci_stages.rb b/db/migrate/20170720111708_add_lock_version_to_ci_stages.rb
new file mode 100644
index 00000000000..e1c4f033286
--- /dev/null
+++ b/db/migrate/20170720111708_add_lock_version_to_ci_stages.rb
@@ -0,0 +1,9 @@
+class AddLockVersionToCiStages < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_stages, :lock_version, :integer
+ end
+end
diff --git a/db/migrate/20170727123534_add_index_on_events_project_id_id.rb b/db/migrate/20170727123534_add_index_on_events_project_id_id.rb
new file mode 100644
index 00000000000..1c4aaaf9dd6
--- /dev/null
+++ b/db/migrate/20170727123534_add_index_on_events_project_id_id.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 AddIndexOnEventsProjectIdId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ COLUMNS = %i[project_id id].freeze
+ TABLES = %i[events events_for_migration].freeze
+
+ disable_ddl_transaction!
+
+ def up
+ TABLES.each do |table|
+ add_concurrent_index(table, COLUMNS) unless index_exists?(table, COLUMNS)
+
+ # We remove the index _after_ adding the new one since MySQL doesn't let
+ # you remove an index when a foreign key exists for the same column.
+ if index_exists?(table, :project_id)
+ remove_concurrent_index(table, :project_id)
+ end
+ end
+ end
+
+ def down
+ TABLES.each do |table|
+ unless index_exists?(table, :project_id)
+ add_concurrent_index(table, :project_id)
+ end
+
+ unless index_exists?(table, COLUMNS)
+ remove_concurrent_index(table, COLUMNS)
+ end
+ end
+ end
+end
diff --git a/db/migrate/20170802013652_add_storage_fields_to_project.rb b/db/migrate/20170802013652_add_storage_fields_to_project.rb
new file mode 100644
index 00000000000..c2381a9d0b2
--- /dev/null
+++ b/db/migrate/20170802013652_add_storage_fields_to_project.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 AddStorageFieldsToProject < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column :projects, :storage_version, :integer, limit: 2
+ end
+
+ def down
+ remove_column :projects, :storage_version
+ end
+end
diff --git a/db/migrate/20170807071105_add_hashed_storage_to_settings.rb b/db/migrate/20170807071105_add_hashed_storage_to_settings.rb
new file mode 100644
index 00000000000..0846557add8
--- /dev/null
+++ b/db/migrate/20170807071105_add_hashed_storage_to_settings.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddHashedStorageToSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :application_settings, :hashed_storage_enabled, :boolean, default: false
+ end
+
+ def down
+ remove_columns :application_settings, :hashed_storage_enabled
+ end
+end
diff --git a/db/migrate/20170809133343_add_broadcast_messages_index.rb b/db/migrate/20170809133343_add_broadcast_messages_index.rb
new file mode 100644
index 00000000000..4ab2ddb059d
--- /dev/null
+++ b/db/migrate/20170809133343_add_broadcast_messages_index.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 AddBroadcastMessagesIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ COLUMNS = %i[starts_at ends_at id].freeze
+
+ def up
+ add_concurrent_index :broadcast_messages, COLUMNS
+ end
+
+ def down
+ remove_concurrent_index :broadcast_messages, COLUMNS
+ end
+end
diff --git a/db/migrate/20170809134534_add_broadcast_message_not_null_constraints.rb b/db/migrate/20170809134534_add_broadcast_message_not_null_constraints.rb
new file mode 100644
index 00000000000..5551fb51a6e
--- /dev/null
+++ b/db/migrate/20170809134534_add_broadcast_message_not_null_constraints.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 AddBroadcastMessageNotNullConstraints < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ COLUMNS = %i[starts_at ends_at created_at updated_at message_html]
+
+ class BroadcastMessage < ActiveRecord::Base
+ self.table_name = 'broadcast_messages'
+ end
+
+ def up
+ COLUMNS.each do |column|
+ BroadcastMessage.where(column => nil).delete_all
+
+ change_column_null :broadcast_messages, column, false
+ end
+ end
+
+ def down
+ COLUMNS.each do |column|
+ change_column_null :broadcast_messages, column, true
+ end
+ end
+end
diff --git a/db/migrate/20170809142252_cleanup_appearances_schema.rb b/db/migrate/20170809142252_cleanup_appearances_schema.rb
new file mode 100644
index 00000000000..acf45060114
--- /dev/null
+++ b/db/migrate/20170809142252_cleanup_appearances_schema.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 CleanupAppearancesSchema < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ NOT_NULL_COLUMNS = %i[title description created_at updated_at]
+
+ TIME_COLUMNS = %i[created_at updated_at]
+
+ def up
+ NOT_NULL_COLUMNS.each do |column|
+ change_column_null :appearances, column, false
+ end
+
+ TIME_COLUMNS.each do |column|
+ change_column :appearances, column, :datetime_with_timezone
+ end
+ end
+
+ def down
+ NOT_NULL_COLUMNS.each do |column|
+ change_column_null :appearances, column, true
+ end
+
+ TIME_COLUMNS.each do |column|
+ change_column :appearances, column, :datetime # rubocop: disable Migration/Datetime
+ end
+ end
+end
diff --git a/db/migrate/20170809161910_add_project_export_enabled_to_application_settings.rb b/db/migrate/20170809161910_add_project_export_enabled_to_application_settings.rb
new file mode 100644
index 00000000000..4baba1ade6d
--- /dev/null
+++ b/db/migrate/20170809161910_add_project_export_enabled_to_application_settings.rb
@@ -0,0 +1,14 @@
+class AddProjectExportEnabledToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:application_settings, :project_export_enabled, :boolean, default: true)
+ end
+
+ def down
+ remove_column(:application_settings, :project_export_enabled)
+ end
+end
diff --git a/db/migrate/20170816133938_add_access_level_to_ci_runners.rb b/db/migrate/20170816133938_add_access_level_to_ci_runners.rb
new file mode 100644
index 00000000000..fc484730f42
--- /dev/null
+++ b/db/migrate/20170816133938_add_access_level_to_ci_runners.rb
@@ -0,0 +1,16 @@
+class AddAccessLevelToCiRunners < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:ci_runners, :access_level, :integer,
+ default: Ci::Runner.access_levels['not_protected'])
+ end
+
+ def down
+ remove_column(:ci_runners, :access_level)
+ end
+end
diff --git a/db/migrate/20170816133940_add_protected_to_ci_builds.rb b/db/migrate/20170816133940_add_protected_to_ci_builds.rb
new file mode 100644
index 00000000000..c73a4387d29
--- /dev/null
+++ b/db/migrate/20170816133940_add_protected_to_ci_builds.rb
@@ -0,0 +1,7 @@
+class AddProtectedToCiBuilds < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds, :protected, :boolean
+ end
+end
diff --git a/db/migrate/20170816143940_add_protected_to_ci_pipelines.rb b/db/migrate/20170816143940_add_protected_to_ci_pipelines.rb
new file mode 100644
index 00000000000..ce8f1e03686
--- /dev/null
+++ b/db/migrate/20170816143940_add_protected_to_ci_pipelines.rb
@@ -0,0 +1,7 @@
+class AddProtectedToCiPipelines < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :ci_pipelines, :protected, :boolean
+ end
+end
diff --git a/db/migrate/20170816153940_add_index_on_ci_builds_protected.rb b/db/migrate/20170816153940_add_index_on_ci_builds_protected.rb
new file mode 100644
index 00000000000..caf7c705a6e
--- /dev/null
+++ b/db/migrate/20170816153940_add_index_on_ci_builds_protected.rb
@@ -0,0 +1,15 @@
+class AddIndexOnCiBuildsProtected < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :ci_builds, :protected
+ end
+
+ def down
+ remove_concurrent_index :ci_builds, :protected if index_exists?(:ci_builds, :protected)
+ end
+end
diff --git a/db/migrate/20170816234252_add_theme_id_to_users.rb b/db/migrate/20170816234252_add_theme_id_to_users.rb
new file mode 100644
index 00000000000..5043f9ec591
--- /dev/null
+++ b/db/migrate/20170816234252_add_theme_id_to_users.rb
@@ -0,0 +1,10 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddThemeIdToUsers < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :users, :theme_id, :integer, limit: 2
+ end
+end
diff --git a/db/migrate/20170817123339_add_verification_status_to_gpg_signatures.rb b/db/migrate/20170817123339_add_verification_status_to_gpg_signatures.rb
new file mode 100644
index 00000000000..128cd109f8d
--- /dev/null
+++ b/db/migrate/20170817123339_add_verification_status_to_gpg_signatures.rb
@@ -0,0 +1,20 @@
+class AddVerificationStatusToGpgSignatures < ActiveRecord::Migration
+ DOWNTIME = false
+
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ # First we remove all signatures because we need to re-verify them all
+ # again anyway (because of the updated verification logic).
+ #
+ # This makes adding the column with default values faster
+ truncate(:gpg_signatures)
+
+ add_column_with_default(:gpg_signatures, :verification_status, :smallint, default: 0)
+ end
+
+ def down
+ remove_column(:gpg_signatures, :verification_status)
+ end
+end
diff --git a/db/migrate/20170820100558_correct_protected_tags_foreign_keys.rb b/db/migrate/20170820100558_correct_protected_tags_foreign_keys.rb
new file mode 100644
index 00000000000..229298e1946
--- /dev/null
+++ b/db/migrate/20170820100558_correct_protected_tags_foreign_keys.rb
@@ -0,0 +1,35 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CorrectProtectedTagsForeignKeys < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ remove_foreign_key_without_error(:protected_tag_create_access_levels,
+ column: :protected_tag_id)
+
+ execute <<-EOF
+ DELETE FROM protected_tag_create_access_levels
+ WHERE NOT EXISTS (
+ SELECT true
+ FROM protected_tags
+ WHERE protected_tag_create_access_levels.protected_tag_id = protected_tags.id
+ )
+ AND protected_tag_id IS NOT NULL
+ EOF
+
+ add_concurrent_foreign_key(:protected_tag_create_access_levels,
+ :protected_tags,
+ column: :protected_tag_id)
+ end
+
+ def down
+ # Previously there was a foreign key without a CASCADING DELETE, so we'll
+ # just leave the foreign key in place.
+ end
+end
diff --git a/db/migrate/20170820120108_create_user_synced_attributes_metadata.rb b/db/migrate/20170820120108_create_user_synced_attributes_metadata.rb
new file mode 100644
index 00000000000..79028e34987
--- /dev/null
+++ b/db/migrate/20170820120108_create_user_synced_attributes_metadata.rb
@@ -0,0 +1,15 @@
+class CreateUserSyncedAttributesMetadata < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :user_synced_attributes_metadata do |t|
+ t.boolean :name_synced, default: false
+ t.boolean :email_synced, default: false
+ t.boolean :location_synced, default: false
+ t.references :user, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.string :provider
+ end
+ end
+end
diff --git a/db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb b/db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb
new file mode 100644
index 00000000000..da518d8215c
--- /dev/null
+++ b/db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb
@@ -0,0 +1,15 @@
+class AddAutoDevopsEnabledToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:application_settings, :auto_devops_enabled, :boolean, default: false)
+ end
+
+ def down
+ remove_column(:application_settings, :auto_devops_enabled, :boolean)
+ end
+end
diff --git a/db/migrate/20170824162758_allow_appearances_description_html_null.rb b/db/migrate/20170824162758_allow_appearances_description_html_null.rb
new file mode 100644
index 00000000000..d7f481ee894
--- /dev/null
+++ b/db/migrate/20170824162758_allow_appearances_description_html_null.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AllowAppearancesDescriptionHtmlNull < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ change_column_null :appearances, :description_html, true
+ end
+
+ def down
+ # This column should not have a `NOT NULL` class, so we don't want to revert
+ # back to re-adding it.
+ end
+end
diff --git a/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb b/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb
new file mode 100644
index 00000000000..c5fb5762d61
--- /dev/null
+++ b/db/migrate/20170825104051_migrate_issues_to_ghost_user.rb
@@ -0,0 +1,37 @@
+class MigrateIssuesToGhostUser < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+ end
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+
+ include ::EachBatch
+ end
+
+ def reset_column_in_migration_models
+ ActiveRecord::Base.clear_cache!
+
+ ::User.reset_column_information
+ ::Namespace.reset_column_information
+ end
+
+ def up
+ reset_column_in_migration_models
+
+ # we use the model method because rewriting it is too complicated and would require copying multiple methods
+ ghost_id = ::User.ghost.id
+
+ Issue.where('NOT EXISTS (?)', User.unscoped.select(1).where('issues.author_id = users.id')).each_batch do |relation|
+ relation.update_all(author_id: ghost_id)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20170825154015_resolve_outdated_diff_discussions.rb b/db/migrate/20170825154015_resolve_outdated_diff_discussions.rb
new file mode 100644
index 00000000000..235530bb1e6
--- /dev/null
+++ b/db/migrate/20170825154015_resolve_outdated_diff_discussions.rb
@@ -0,0 +1,9 @@
+class ResolveOutdatedDiffDiscussions < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column(:projects, :resolve_outdated_diff_discussions, :boolean)
+ end
+end
diff --git a/db/migrate/20170828093725_create_project_auto_dev_ops.rb b/db/migrate/20170828093725_create_project_auto_dev_ops.rb
new file mode 100644
index 00000000000..c1bb4f20c1d
--- /dev/null
+++ b/db/migrate/20170828093725_create_project_auto_dev_ops.rb
@@ -0,0 +1,19 @@
+class CreateProjectAutoDevOps < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ create_table :project_auto_devops do |t|
+ t.belongs_to :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+ t.boolean :enabled, default: nil, null: true
+ t.string :domain
+ end
+ end
+
+ def down
+ drop_table(:project_auto_devops)
+ end
+end
diff --git a/db/migrate/20170828135939_migrate_user_external_mail_data.rb b/db/migrate/20170828135939_migrate_user_external_mail_data.rb
new file mode 100644
index 00000000000..592e141b7e6
--- /dev/null
+++ b/db/migrate/20170828135939_migrate_user_external_mail_data.rb
@@ -0,0 +1,57 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MigrateUserExternalMailData < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+
+ include EachBatch
+ end
+
+ class UserSyncedAttributesMetadata < ActiveRecord::Base
+ self.table_name = 'user_synced_attributes_metadata'
+
+ include EachBatch
+ end
+
+ def up
+ User.each_batch do |batch|
+ start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
+
+ execute <<-EOF
+ INSERT INTO user_synced_attributes_metadata (user_id, provider, email_synced)
+ SELECT id, email_provider, external_email
+ FROM users
+ WHERE external_email = TRUE
+ AND NOT EXISTS (
+ SELECT true
+ FROM user_synced_attributes_metadata
+ WHERE user_id = users.id
+ AND provider = users.email_provider
+ )
+ AND id BETWEEN #{start_id} AND #{end_id}
+ EOF
+ end
+ end
+
+ def down
+ UserSyncedAttributesMetadata.each_batch do |batch|
+ start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
+
+ execute <<-EOF
+ UPDATE users
+ SET users.email_provider = metadata.provider, users.external_email = metadata.email_synced
+ FROM user_synced_attributes_metadata as metadata, users
+ WHERE metadata.email_synced = TRUE
+ AND metadata.user_id = users.id
+ AND id BETWEEN #{start_id} AND #{end_id}
+ EOF
+ end
+ end
+end
diff --git a/db/migrate/20170830125940_add_failure_reason_to_ci_builds.rb b/db/migrate/20170830125940_add_failure_reason_to_ci_builds.rb
new file mode 100644
index 00000000000..5a7487b9227
--- /dev/null
+++ b/db/migrate/20170830125940_add_failure_reason_to_ci_builds.rb
@@ -0,0 +1,9 @@
+class AddFailureReasonToCiBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds, :failure_reason, :integer
+ end
+end
diff --git a/db/migrate/20170830130119_steal_remaining_event_migration_jobs.rb b/db/migrate/20170830130119_steal_remaining_event_migration_jobs.rb
new file mode 100644
index 00000000000..0dfdc4ed261
--- /dev/null
+++ b/db/migrate/20170830130119_steal_remaining_event_migration_jobs.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class StealRemainingEventMigrationJobs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ Gitlab::BackgroundMigration.steal('MigrateEventsToPushEventPayloads')
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20170830131015_swap_event_migration_tables.rb b/db/migrate/20170830131015_swap_event_migration_tables.rb
new file mode 100644
index 00000000000..5128d1b2fe7
--- /dev/null
+++ b/db/migrate/20170830131015_swap_event_migration_tables.rb
@@ -0,0 +1,23 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class SwapEventMigrationTables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ rename_tables
+ end
+
+ def down
+ rename_tables
+ end
+
+ def rename_tables
+ rename_table :events, :events_old
+ rename_table :events_for_migration, :events
+ rename_table :events_old, :events_for_migration
+ end
+end
diff --git a/db/migrate/20170831092813_add_config_source_to_pipelines.rb b/db/migrate/20170831092813_add_config_source_to_pipelines.rb
new file mode 100644
index 00000000000..ff51e968abd
--- /dev/null
+++ b/db/migrate/20170831092813_add_config_source_to_pipelines.rb
@@ -0,0 +1,7 @@
+class AddConfigSourceToPipelines < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column(:ci_pipelines, :config_source, :integer, allow_null: true)
+ end
+end
diff --git a/db/migrate/20170901071411_add_foreign_key_to_issue_author.rb b/db/migrate/20170901071411_add_foreign_key_to_issue_author.rb
new file mode 100644
index 00000000000..ab6e9fb565a
--- /dev/null
+++ b/db/migrate/20170901071411_add_foreign_key_to_issue_author.rb
@@ -0,0 +1,14 @@
+class AddForeignKeyToIssueAuthor < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key(:issues, :users, column: :author_id, on_delete: :nullify)
+ end
+
+ def down
+ remove_foreign_key(:issues, column: :author_id)
+ end
+end
diff --git a/db/migrate/20170905112933_add_resolved_by_push_to_notes.rb b/db/migrate/20170905112933_add_resolved_by_push_to_notes.rb
new file mode 100644
index 00000000000..ceb31ffb08a
--- /dev/null
+++ b/db/migrate/20170905112933_add_resolved_by_push_to_notes.rb
@@ -0,0 +1,9 @@
+class AddResolvedByPushToNotes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :notes, :resolved_by_push, :boolean
+ end
+end
diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb
index be3501c4c2e..5cd9f3198e3 100644
--- a/db/migrate/limits_to_mysql.rb
+++ b/db/migrate/limits_to_mysql.rb
@@ -7,6 +7,5 @@ class LimitsToMysql < ActiveRecord::Migration
change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
change_column :snippets, :content, :text, limit: 2147483647
change_column :notes, :st_diff, :text, limit: 2147483647
- change_column :events, :data, :text, limit: 2147483647
end
end
diff --git a/db/post_migrate/20170317162059_update_upload_paths_to_system.rb b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb
index ca2912f8dce..92e33848bf0 100644
--- a/db/post_migrate/20170317162059_update_upload_paths_to_system.rb
+++ b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb
@@ -48,7 +48,7 @@ class UpdateUploadPathsToSystem < ActiveRecord::Migration
end
def new_upload_dir
- File.join(base_directory, "system")
+ File.join(base_directory, "-", "system")
end
def arel_table
diff --git a/db/post_migrate/20170406111121_clean_upload_symlinks.rb b/db/post_migrate/20170406111121_clean_upload_symlinks.rb
index fc3a4acc0bb..f2ce25d4524 100644
--- a/db/post_migrate/20170406111121_clean_upload_symlinks.rb
+++ b/db/post_migrate/20170406111121_clean_upload_symlinks.rb
@@ -47,6 +47,6 @@ class CleanUploadSymlinks < ActiveRecord::Migration
end
def new_upload_dir
- File.join(base_directory, "public", "uploads", "system")
+ File.join(base_directory, "public", "uploads", "-", "system")
end
end
diff --git a/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb b/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb
index c1e64f20109..5238a2ba1b7 100644
--- a/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb
+++ b/db/post_migrate/20170502101023_cleanup_namespaceless_pending_delete_projects.rb
@@ -30,7 +30,7 @@ class CleanupNamespacelessPendingDeleteProjects < ActiveRecord::Migration
private
def pending_delete_batch
- connection.exec_query(find_batch).map{ |row| row['id'].to_i }
+ connection.exec_query(find_batch).map { |row| row['id'].to_i }
end
BATCH_SIZE = 5000
diff --git a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb
index 705e11ed47d..9d9f36550e7 100644
--- a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb
+++ b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb
@@ -21,7 +21,7 @@ class UpdateRetriedForCiBuild < ActiveRecord::Migration
private
def up_mysql
- # This is a trick to overcome MySQL limitation:
+ # This is a trick to overcome MySQL limitation:
# Mysql2::Error: Table 'ci_builds' is specified twice, both as a target for 'UPDATE' and as a separate source for data
# However, this leads to create a temporary table from `max(ci_builds.id)` which is slow and do full database update
execute <<-SQL.strip_heredoc
@@ -54,14 +54,14 @@ class UpdateRetriedForCiBuild < ActiveRecord::Migration
def with_temporary_partial_index
if Gitlab::Database.postgresql?
- unless index_exists?(:ci_builds, name: :index_for_ci_builds_retried_migration)
+ unless index_exists?(:ci_builds, :id, name: :index_for_ci_builds_retried_migration)
execute 'CREATE INDEX CONCURRENTLY index_for_ci_builds_retried_migration ON ci_builds (id) WHERE retried IS NULL;'
end
end
yield
- if Gitlab::Database.postgresql? && index_exists?(:ci_builds, name: :index_for_ci_builds_retried_migration)
+ if Gitlab::Database.postgresql? && index_exists?(:ci_builds, :id, name: :index_for_ci_builds_retried_migration)
execute 'DROP INDEX CONCURRENTLY index_for_ci_builds_retried_migration'
end
end
diff --git a/db/post_migrate/20170523083112_migrate_old_artifacts.rb b/db/post_migrate/20170523083112_migrate_old_artifacts.rb
index f2690bd0017..3a77b9751d3 100644
--- a/db/post_migrate/20170523083112_migrate_old_artifacts.rb
+++ b/db/post_migrate/20170523083112_migrate_old_artifacts.rb
@@ -7,7 +7,7 @@ class MigrateOldArtifacts < ActiveRecord::Migration
# This uses special heuristic to find potential candidates for data migration
# Read more about this here: https://gitlab.com/gitlab-org/gitlab-ce/issues/32036#note_30422345
-
+
def up
builds_with_artifacts.find_each do |build|
build.migrate_artifacts!
@@ -51,14 +51,14 @@ class MigrateOldArtifacts < ActiveRecord::Migration
private
def source_artifacts_path
- @source_artifacts_path ||=
+ @source_artifacts_path ||=
File.join(Gitlab.config.artifacts.path,
created_at.utc.strftime('%Y_%m'),
ci_id.to_s, id.to_s)
end
def target_artifacts_path
- @target_artifacts_path ||=
+ @target_artifacts_path ||=
File.join(Gitlab.config.artifacts.path,
created_at.utc.strftime('%Y_%m'),
project_id.to_s, id.to_s)
diff --git a/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb b/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb
index 561de59ec69..07935ab8a52 100644
--- a/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb
+++ b/db/post_migrate/20170606202615_move_appearance_to_system_dir.rb
@@ -52,6 +52,6 @@ class MoveAppearanceToSystemDir < ActiveRecord::Migration
end
def new_upload_dir
- File.join(base_directory, "public", "uploads", "system")
+ File.join(base_directory, "public", "uploads", "-", "system")
end
end
diff --git a/db/post_migrate/20170612071012_move_personal_snippets_files.rb b/db/post_migrate/20170612071012_move_personal_snippets_files.rb
index 33043364bde..2b79a87ccd8 100644
--- a/db/post_migrate/20170612071012_move_personal_snippets_files.rb
+++ b/db/post_migrate/20170612071012_move_personal_snippets_files.rb
@@ -10,7 +10,7 @@ class MovePersonalSnippetsFiles < ActiveRecord::Migration
return unless file_storage?
@source_relative_location = File.join('/uploads', 'personal_snippet')
- @destination_relative_location = File.join('/uploads', 'system', 'personal_snippet')
+ @destination_relative_location = File.join('/uploads', '-', 'system', 'personal_snippet')
move_personal_snippet_files
end
@@ -18,7 +18,7 @@ class MovePersonalSnippetsFiles < ActiveRecord::Migration
def down
return unless file_storage?
- @source_relative_location = File.join('/uploads', 'system', 'personal_snippet')
+ @source_relative_location = File.join('/uploads', '-', 'system', 'personal_snippet')
@destination_relative_location = File.join('/uploads', 'personal_snippet')
move_personal_snippet_files
diff --git a/db/post_migrate/20170627101016_schedule_event_migrations.rb b/db/post_migrate/20170627101016_schedule_event_migrations.rb
new file mode 100644
index 00000000000..1f34375ff0d
--- /dev/null
+++ b/db/post_migrate/20170627101016_schedule_event_migrations.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 ScheduleEventMigrations < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BUFFER_SIZE = 1000
+
+ disable_ddl_transaction!
+
+ class Event < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'events'
+ end
+
+ def up
+ jobs = []
+
+ Event.each_batch(of: 1000) do |relation|
+ min, max = relation.pluck('MIN(id), MAX(id)').first
+
+ if jobs.length == BUFFER_SIZE
+ # 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 << ['MigrateEventsToPushEventPayloads', [min, max]]
+ end
+
+ BackgroundMigrationWorker.perform_bulk(jobs) unless jobs.empty?
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20170711145558_migrate_stages_statuses.rb b/db/post_migrate/20170711145558_migrate_stages_statuses.rb
new file mode 100644
index 00000000000..aeb900354db
--- /dev/null
+++ b/db/post_migrate/20170711145558_migrate_stages_statuses.rb
@@ -0,0 +1,33 @@
+class MigrateStagesStatuses < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ BATCH_SIZE = 10000
+ RANGE_SIZE = 100
+ MIGRATION = 'MigrateStageStatus'.freeze
+
+ class Stage < ActiveRecord::Base
+ self.table_name = 'ci_stages'
+ include ::EachBatch
+ end
+
+ def up
+ Stage.where(status: nil).each_batch(of: BATCH_SIZE) do |relation, index|
+ relation.each_batch(of: RANGE_SIZE) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+ delay = index * 5.minutes
+
+ BackgroundMigrationWorker.perform_in(delay, MIGRATION, range)
+ end
+ end
+ end
+
+ def down
+ disable_statement_timeout
+
+ update_column_in_batches(:ci_stages, :status, nil)
+ end
+end
diff --git a/db/post_migrate/20170807190736_move_personal_snippet_files_into_correct_folder.rb b/db/post_migrate/20170807190736_move_personal_snippet_files_into_correct_folder.rb
new file mode 100644
index 00000000000..e3d2446b897
--- /dev/null
+++ b/db/post_migrate/20170807190736_move_personal_snippet_files_into_correct_folder.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 MovePersonalSnippetFilesIntoCorrectFolder < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+ NEW_DIRECTORY = File.join('/uploads', '-', 'system', 'personal_snippet')
+ OLD_DIRECTORY = File.join('/uploads', 'system', 'personal_snippet')
+
+ def up
+ return unless file_storage?
+
+ BackgroundMigrationWorker.perform_async('MovePersonalSnippetFiles',
+ [OLD_DIRECTORY, NEW_DIRECTORY])
+ end
+
+ def down
+ return unless file_storage?
+
+ BackgroundMigrationWorker.perform_async('MovePersonalSnippetFiles',
+ [NEW_DIRECTORY, OLD_DIRECTORY])
+ end
+
+ def file_storage?
+ CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File
+ end
+end
diff --git a/db/post_migrate/20170815060945_remove_duplicate_mr_events.rb b/db/post_migrate/20170815060945_remove_duplicate_mr_events.rb
new file mode 100644
index 00000000000..6132b553177
--- /dev/null
+++ b/db/post_migrate/20170815060945_remove_duplicate_mr_events.rb
@@ -0,0 +1,26 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveDuplicateMrEvents < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ class Event < ActiveRecord::Base
+ self.table_name = 'events'
+ end
+
+ def up
+ base_condition = "action = 1 AND target_type = 'MergeRequest' AND created_at > '2017-08-13'"
+ Event.select('target_id, count(*)')
+ .where(base_condition)
+ .group('target_id').having('count(*) > 1').each do |event|
+ duplicates = Event.where("#{base_condition} AND target_id = #{event.target_id}").pluck(:id)
+ duplicates.shift
+
+ Event.where(id: duplicates).delete_all
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20170816102555_cleanup_nonexisting_namespace_pending_delete_projects.rb b/db/post_migrate/20170816102555_cleanup_nonexisting_namespace_pending_delete_projects.rb
new file mode 100644
index 00000000000..3f085c17133
--- /dev/null
+++ b/db/post_migrate/20170816102555_cleanup_nonexisting_namespace_pending_delete_projects.rb
@@ -0,0 +1,54 @@
+# Follow up of CleanupNamespacelessPendingDeleteProjects and it cleans
+# all projects with `pending_delete = true` and for which the
+# namespace no longer exists.
+class CleanupNonexistingNamespacePendingDeleteProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ include ::EachBatch
+ end
+
+ class Namespace < ActiveRecord::Base
+ self.table_name = 'namespaces'
+ end
+
+ def up
+ find_projects.each_batch do |batch|
+ args = batch.pluck(:id).map { |id| [id] }
+
+ NamespacelessProjectDestroyWorker.bulk_perform_async(args)
+ end
+ end
+
+ def down
+ # NOOP
+ end
+
+ private
+
+ def find_projects
+ projects = Project.arel_table
+ namespaces = Namespace.arel_table
+
+ namespace_query = namespaces.project(1)
+ .where(namespaces[:id].eq(projects[:namespace_id]))
+ .exists.not
+
+ # SELECT "projects"."id"
+ # FROM "projects"
+ # WHERE "projects"."pending_delete" = 't'
+ # AND (NOT (EXISTS
+ # (SELECT 1
+ # FROM "namespaces"
+ # WHERE "namespaces"."id" = "projects"."namespace_id")))
+ Project.where(projects[:pending_delete].eq(true))
+ .where(namespace_query)
+ .select(:id)
+ end
+end
diff --git a/db/post_migrate/20170822101017_migrate_pipeline_sidekiq_queues.rb b/db/post_migrate/20170822101017_migrate_pipeline_sidekiq_queues.rb
new file mode 100644
index 00000000000..8441cfe7968
--- /dev/null
+++ b/db/post_migrate/20170822101017_migrate_pipeline_sidekiq_queues.rb
@@ -0,0 +1,17 @@
+class MigratePipelineSidekiqQueues < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ sidekiq_queue_migrate 'build', to: 'pipeline_default'
+ sidekiq_queue_migrate 'pipeline', to: 'pipeline_default'
+ end
+
+ def down
+ sidekiq_queue_migrate 'pipeline_default', to: 'pipeline'
+ sidekiq_queue_migrate 'pipeline_processing', to: 'pipeline'
+ sidekiq_queue_migrate 'pipeline_hooks', to: 'pipeline'
+ sidekiq_queue_migrate 'pipeline_cache', to: 'pipeline'
+ 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
new file mode 100644
index 00000000000..fefd931e5d2
--- /dev/null
+++ b/db/post_migrate/20170828170502_post_deploy_migrate_user_external_mail_data.rb
@@ -0,0 +1,57 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PostDeployMigrateUserExternalMailData < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+
+ include EachBatch
+ end
+
+ class UserSyncedAttributesMetadata < ActiveRecord::Base
+ self.table_name = 'user_synced_attributes_metadata'
+
+ include EachBatch
+ end
+
+ def up
+ User.each_batch do |batch|
+ start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
+
+ execute <<-EOF
+ INSERT INTO user_synced_attributes_metadata (user_id, provider, email_synced)
+ SELECT id, email_provider, external_email
+ FROM users
+ WHERE external_email = TRUE
+ AND NOT EXISTS (
+ SELECT true
+ FROM user_synced_attributes_metadata
+ WHERE user_id = users.id
+ AND provider = users.email_provider
+ )
+ AND id BETWEEN #{start_id} AND #{end_id}
+ EOF
+ end
+ end
+
+ def down
+ UserSyncedAttributesMetadata.each_batch do |batch|
+ start_id, end_id = batch.pluck('MIN(id), MAX(id)').first
+
+ execute <<-EOF
+ UPDATE users
+ SET users.email_provider = metadata.provider, users.external_email = metadata.email_synced
+ FROM user_synced_attributes_metadata as metadata, users
+ WHERE metadata.email_synced = TRUE
+ AND metadata.user_id = users.id
+ AND id BETWEEN #{start_id} AND #{end_id}
+ EOF
+ end
+ end
+end
diff --git a/db/post_migrate/20170828170513_remove_user_email_provider_column.rb b/db/post_migrate/20170828170513_remove_user_email_provider_column.rb
new file mode 100644
index 00000000000..570f2b3772a
--- /dev/null
+++ b/db/post_migrate/20170828170513_remove_user_email_provider_column.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUserEmailProviderColumn < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ remove_column :users, :email_provider, :string
+ end
+end
diff --git a/db/post_migrate/20170828170516_remove_user_external_mail_columns.rb b/db/post_migrate/20170828170516_remove_user_external_mail_columns.rb
new file mode 100644
index 00000000000..bb81dc682b3
--- /dev/null
+++ b/db/post_migrate/20170828170516_remove_user_external_mail_columns.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUserExternalMailColumns < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ remove_column :users, :external_email, :boolean
+ end
+end
diff --git a/db/post_migrate/20170830084744_destroy_gpg_signatures.rb b/db/post_migrate/20170830084744_destroy_gpg_signatures.rb
new file mode 100644
index 00000000000..b04d36f6537
--- /dev/null
+++ b/db/post_migrate/20170830084744_destroy_gpg_signatures.rb
@@ -0,0 +1,10 @@
+class DestroyGpgSignatures < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ truncate(:gpg_signatures)
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20170830150306_drop_events_for_migration_table.rb b/db/post_migrate/20170830150306_drop_events_for_migration_table.rb
new file mode 100644
index 00000000000..763ee9a810d
--- /dev/null
+++ b/db/post_migrate/20170830150306_drop_events_for_migration_table.rb
@@ -0,0 +1,48 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class DropEventsForMigrationTable < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Event < ActiveRecord::Base
+ include EachBatch
+ end
+
+ def up
+ transaction do
+ drop_table :events_for_migration
+ end
+ end
+
+ # rubocop: disable Migration/Datetime
+ def down
+ create_table :events_for_migration do |t|
+ t.string :target_type, index: true
+ t.integer :target_id, index: true
+ t.string :title
+ t.text :data
+ t.integer :project_id
+ t.datetime :created_at, index: true
+ t.datetime :updated_at
+ t.integer :action, index: true
+ t.integer :author_id, index: true
+
+ t.index [:project_id, :id]
+ end
+
+ Event.all.each_batch do |relation|
+ start_id, stop_id = relation.pluck('MIN(id), MAX(id)').first
+
+ execute <<-EOF.strip_heredoc
+ INSERT INTO events_for_migration (target_type, target_id, project_id, created_at, updated_at, action, author_id)
+ SELECT target_type, target_id, project_id, created_at, updated_at, action, author_id
+ FROM events
+ WHERE id BETWEEN #{start_id} AND #{stop_id}
+ EOF
+ end
+ end
+end
diff --git a/db/post_migrate/20170831195038_remove_valid_signature_from_gpg_signatures.rb b/db/post_migrate/20170831195038_remove_valid_signature_from_gpg_signatures.rb
new file mode 100644
index 00000000000..9b6745e33d9
--- /dev/null
+++ b/db/post_migrate/20170831195038_remove_valid_signature_from_gpg_signatures.rb
@@ -0,0 +1,11 @@
+class RemoveValidSignatureFromGpgSignatures < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ remove_column :gpg_signatures, :valid_signature
+ end
+
+ def down
+ add_column :gpg_signatures, :valid_signature, :boolean
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ffa165c3444..74634d14ccb 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: 20170807160457) do
+ActiveRecord::Schema.define(version: 20170905112933) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -28,12 +28,12 @@ ActiveRecord::Schema.define(version: 20170807160457) do
end
create_table "appearances", force: :cascade do |t|
- t.string "title"
- t.text "description"
+ t.string "title", null: false
+ t.text "description", null: false
t.string "header_logo"
t.string "logo"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
t.text "description_html"
t.integer "cached_markdown_version"
end
@@ -101,6 +101,10 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.text "help_page_text_html"
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
+ t.integer "rsa_key_restriction", default: 0, null: false
+ t.integer "dsa_key_restriction", default: 0, null: false
+ t.integer "ecdsa_key_restriction", default: 0, null: false
+ t.integer "ed25519_key_restriction", default: 0, null: false
t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false
@@ -125,8 +129,11 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.boolean "prometheus_metrics_enabled", default: false, null: false
t.boolean "help_page_hide_commercial_content", default: false
t.string "help_page_support_url"
- t.integer "performance_bar_allowed_group_id"
t.boolean "password_authentication_enabled"
+ t.integer "performance_bar_allowed_group_id"
+ t.boolean "hashed_storage_enabled", default: false, null: false
+ t.boolean "project_export_enabled", default: true, null: false
+ t.boolean "auto_devops_enabled", default: false, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -163,16 +170,18 @@ ActiveRecord::Schema.define(version: 20170807160457) do
create_table "broadcast_messages", force: :cascade do |t|
t.text "message", null: false
- t.datetime "starts_at"
- t.datetime "ends_at"
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime "starts_at", null: false
+ t.datetime "ends_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.string "color"
t.string "font"
- t.text "message_html"
+ t.text "message_html", null: false
t.integer "cached_markdown_version"
end
+ add_index "broadcast_messages", ["starts_at", "ends_at", "id"], name: "index_broadcast_messages_on_starts_at_and_ends_at_and_id", using: :btree
+
create_table "chat_names", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "service_id", null: false
@@ -240,6 +249,8 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.integer "stage_id"
t.integer "artifacts_file_store", default: 1, null: false
t.integer "artifacts_metadata_store", default: 1, null: false
+ t.boolean "protected"
+ t.integer "failure_reason"
end
add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
@@ -247,7 +258,9 @@ ActiveRecord::Schema.define(version: 20170807160457) 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", ["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
add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
@@ -264,8 +277,8 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.string "encrypted_value_iv"
t.integer "group_id", null: false
t.boolean "protected", default: false, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
end
add_index "ci_group_variables", ["group_id", "key"], name: "index_ci_group_variables_on_group_id_and_key", unique: true, using: :btree
@@ -277,8 +290,8 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.string "encrypted_value_salt"
t.string "encrypted_value_iv"
t.integer "pipeline_schedule_id", null: false
- t.datetime "created_at"
- t.datetime "updated_at"
+ t.datetime_with_timezone "created_at"
+ t.datetime_with_timezone "updated_at"
end
add_index "ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree
@@ -330,6 +343,8 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.integer "auto_canceled_by_id"
t.integer "pipeline_schedule_id"
t.integer "source"
+ t.integer "config_source"
+ t.boolean "protected"
end
add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
@@ -365,6 +380,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.string "architecture"
t.boolean "run_untagged", default: true, null: false
t.boolean "locked", default: false, null: false
+ t.integer "access_level", default: 0, null: false
end
add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
@@ -378,6 +394,8 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "name"
+ t.integer "status"
+ t.integer "lock_version"
end
add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", using: :btree
@@ -518,23 +536,19 @@ ActiveRecord::Schema.define(version: 20170807160457) do
add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree
create_table "events", force: :cascade do |t|
- t.string "target_type"
- t.integer "target_id"
- t.string "title"
- t.text "data"
t.integer "project_id"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.integer "action"
- t.integer "author_id"
+ t.integer "author_id", null: false
+ t.integer "target_id"
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "action", limit: 2, null: false
+ t.string "target_type"
end
add_index "events", ["action"], name: "index_events_on_action", using: :btree
add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree
- add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree
- add_index "events", ["project_id"], name: "index_events_on_project_id", using: :btree
- add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree
- add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree
+ add_index "events", ["project_id", "id"], name: "index_events_on_project_id_and_id", using: :btree
+ add_index "events", ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id", using: :btree
create_table "feature_gates", force: :cascade do |t|
t.string "feature_key", null: false
@@ -564,8 +578,8 @@ ActiveRecord::Schema.define(version: 20170807160457) 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 "gpg_keys", force: :cascade do |t|
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
t.integer "user_id"
t.binary "primary_keyid"
t.binary "fingerprint"
@@ -577,15 +591,15 @@ ActiveRecord::Schema.define(version: 20170807160457) do
add_index "gpg_keys", ["user_id"], name: "index_gpg_keys_on_user_id", using: :btree
create_table "gpg_signatures", force: :cascade do |t|
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
t.integer "project_id"
t.integer "gpg_key_id"
- t.boolean "valid_signature"
t.binary "commit_sha"
t.binary "gpg_key_primary_keyid"
t.text "gpg_key_user_name"
t.text "gpg_key_user_email"
+ t.integer "verification_status", limit: 2, default: 0, null: false
end
add_index "gpg_signatures", ["commit_sha"], name: "index_gpg_signatures_on_commit_sha", unique: true, using: :btree
@@ -776,8 +790,8 @@ ActiveRecord::Schema.define(version: 20170807160457) do
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
create_table "merge_request_diff_commits", id: false, force: :cascade do |t|
- t.datetime "authored_date"
- t.datetime "committed_date"
+ t.datetime_with_timezone "authored_date"
+ t.datetime_with_timezone "committed_date"
t.integer "merge_request_diff_id", null: false
t.integer "relative_order", null: false
t.binary "sha", null: false
@@ -974,6 +988,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.text "note_html"
t.integer "cached_markdown_version"
t.text "change_position"
+ t.boolean "resolved_by_push"
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
@@ -1097,6 +1112,16 @@ ActiveRecord::Schema.define(version: 20170807160457) do
add_index "project_authorizations", ["project_id"], name: "index_project_authorizations_on_project_id", using: :btree
add_index "project_authorizations", ["user_id", "project_id", "access_level"], name: "index_project_authorizations_on_user_id_project_id_access_level", unique: true, using: :btree
+ create_table "project_auto_devops", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.boolean "enabled"
+ t.string "domain"
+ end
+
+ add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
+
create_table "project_features", force: :cascade do |t|
t.integer "project_id"
t.integer "merge_requests_access_level"
@@ -1180,6 +1205,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.string "repository_storage", default: "default", null: false
t.boolean "request_access_enabled", default: false, null: false
t.boolean "has_external_wiki"
+ t.string "ci_config_path"
t.boolean "lfs_enabled"
t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved"
@@ -1187,9 +1213,10 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.integer "auto_cancel_pending_pipelines", default: 1, null: false
t.string "import_jid"
t.integer "cached_markdown_version"
- t.datetime "last_repository_updated_at"
- t.string "ci_config_path"
t.text "delete_error"
+ t.datetime "last_repository_updated_at"
+ t.integer "storage_version", limit: 2
+ t.boolean "resolve_outdated_diff_discussions"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1256,6 +1283,19 @@ ActiveRecord::Schema.define(version: 20170807160457) do
add_index "protected_tags", ["project_id"], name: "index_protected_tags_on_project_id", using: :btree
+ create_table "push_event_payloads", id: false, force: :cascade do |t|
+ t.integer "commit_count", limit: 8, null: false
+ t.integer "event_id", null: false
+ t.integer "action", limit: 2, null: false
+ t.integer "ref_type", limit: 2, null: false
+ t.binary "commit_from"
+ t.binary "commit_to"
+ t.text "ref"
+ t.string "commit_title", limit: 70
+ end
+
+ add_index "push_event_payloads", ["event_id"], name: "index_push_event_payloads_on_event_id", unique: true, using: :btree
+
create_table "redirect_routes", force: :cascade do |t|
t.integer "source_id", null: false
t.string "source_type", null: false
@@ -1495,6 +1535,16 @@ ActiveRecord::Schema.define(version: 20170807160457) 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_synced_attributes_metadata", force: :cascade do |t|
+ t.boolean "name_synced", default: false
+ t.boolean "email_synced", default: false
+ t.boolean "location_synced", default: false
+ t.integer "user_id", null: false
+ t.string "provider"
+ end
+
+ add_index "user_synced_attributes_metadata", ["user_id"], name: "index_user_synced_attributes_metadata_on_user_id", unique: true, using: :btree
+
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@@ -1560,8 +1610,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do
t.boolean "notified_of_own_activity"
t.string "preferred_language"
t.string "rss_token"
- t.boolean "external_email", default: false, null: false
- t.string "email_provider"
+ t.integer "theme_id", limit: 2
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -1655,7 +1704,8 @@ ActiveRecord::Schema.define(version: 20170807160457) do
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade
- add_foreign_key "events", "projects", name: "fk_0434b48643", 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 "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade
add_foreign_key "gpg_keys", "users", on_delete: :cascade
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
@@ -1664,6 +1714,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade
+ add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
@@ -1687,6 +1738,7 @@ ActiveRecord::Schema.define(version: 20170807160457) do
add_foreign_key "personal_access_tokens", "users"
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
+ add_foreign_key "project_auto_devops", "projects", on_delete: :cascade
add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
@@ -1695,9 +1747,10 @@ ActiveRecord::Schema.define(version: 20170807160457) do
add_foreign_key "protected_branch_push_access_levels", "protected_branches", name: "fk_9ffc86a3d9", on_delete: :cascade
add_foreign_key "protected_branches", "projects", name: "fk_7a9c6d93e7", on_delete: :cascade
add_foreign_key "protected_tag_create_access_levels", "namespaces", column: "group_id"
- add_foreign_key "protected_tag_create_access_levels", "protected_tags"
+ add_foreign_key "protected_tag_create_access_levels", "protected_tags", name: "fk_f7dfda8c51", on_delete: :cascade
add_foreign_key "protected_tag_create_access_levels", "users"
add_foreign_key "protected_tags", "projects", name: "fk_8e4af87648", on_delete: :cascade
+ add_foreign_key "push_event_payloads", "events", name: "fk_36c74129da", on_delete: :cascade
add_foreign_key "releases", "projects", name: "fk_47fe2a0596", on_delete: :cascade
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
@@ -1708,6 +1761,7 @@ ActiveRecord::Schema.define(version: 20170807160457) 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_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
add_foreign_key "web_hooks", "projects", name: "fk_0c8ca6d9d1", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index 8bb8e147cd1..b3e7c9bd0bf 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,9 +1,13 @@
+---
+toc: false
+---
+
# GitLab Documentation
Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured
platform for software development!
-We offer four different products for you and your company:
+GitLab offers the most scalable Git-based fully integrated platform for software development, with flexible products and subscription plans:
- **GitLab Community Edition (CE)** is an [opensource product](https://gitlab.com/gitlab-org/gitlab-ce/),
self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com.
@@ -20,12 +24,16 @@ plus premium features available in each version: **Enterprise Edition Starter**
Shortcuts to GitLab's most visited docs:
-| [GitLab CI](ci/README.md) | Other |
+| [GitLab CI/CD](ci/README.md) | Other |
| :----- | :----- |
| [Quick start guide](ci/quick_start/README.md) | [API](api/README.md) |
| [Configuring `.gitlab-ci.yml`](ci/yaml/README.md) | [SSH authentication](ssh/README.md) |
| [Using Docker images](ci/docker/using_docker_images.md) | [GitLab Pages](user/project/pages/index.md) |
+- [User documentation](user/index.md)
+- [Administrator documentation](#administrator-documentation)
+- [Technical Articles](articles/index.md)
+
## Getting started with GitLab
- [GitLab Basics](gitlab-basics/README.md): Start working on your command line and on GitLab.
@@ -33,10 +41,10 @@ Shortcuts to GitLab's most visited docs:
- See also [GitLab Workflow - an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
- [GitLab Markdown](user/markdown.md): GitLab's advanced formatting system (GitLab Flavored Markdown).
- [GitLab Quick Actions](user/project/quick_actions.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI.
+- [Auto DevOps](topics/autodevops/index.md)
### User account
-- [User documentation](user/index.md): Learn how to use GitLab and explore its features
- [User account](user/profile/index.md): Manage your account
- [Authentication](topics/authentication/index.md): Account security with two-factor authentication, setup your ssh keys and deploy keys for secure access to your projects.
- [Profile settings](user/profile/index.md#profile-settings): Manage your profile settings, two factor authentication and more.
@@ -70,12 +78,14 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
- [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
+ - [Signing commits](user/project/repository/gpg_signed_commits/index.md): use GPG to sign your commits.
### Issues and Merge Requests (MRs)
- [Discussions](user/discussions/index.md) Threads, comments, and resolvable discussions in issues, commits, and merge requests.
- [Issues](user/project/issues/index.md)
-- [Issue Board](user/project/issue_board.md)
+- [Project issue Board](user/project/issue_board.md)
- [Issues and merge requests templates](user/project/description_templates.md): Create templates for submitting new issues and merge requests.
- [Labels](user/project/labels.md): Categorize your issues or merge requests based on descriptive titles.
- [Merge Requests](user/project/merge_requests/index.md)
@@ -91,11 +101,10 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
- [Git](topics/git/index.md): Getting started with Git, branching strategies, Git LFS, advanced use.
- [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf): Download a PDF describing the most used Git operations.
- [GitLab Flow](workflow/gitlab_flow.md): explore the best of Git with the GitLab Flow strategy.
-- [Signing commits](workflow/gpg_signed_commits/index.md): use GPG to sign your commits.
### Migrate and import your projects from other platforms
-- [Importing to GitLab](workflow/importing/README.md): Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
+- [Importing to GitLab](user/project/import/index.md): Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
- [Migrating from SVN](workflow/importing/migrating_from_svn.md): Convert a SVN repository to Git and GitLab.
### Continuous Integration, Delivery, and Deployment
@@ -152,7 +161,6 @@ have access to GitLab administration tools and settings.
### Integrations
- [Integrations](integration/README.md): How to integrate with systems such as JIRA, Redmine, Twitter.
-- [Koding](administration/integration/koding.md): Set up Koding to use with GitLab.
- [Mattermost](user/project/integrations/mattermost.md): Set up GitLab with Mattermost.
### Monitoring
diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md
index 1528f1d2b17..252ff1f4b15 100644
--- a/doc/administration/auth/authentiq.md
+++ b/doc/administration/auth/authentiq.md
@@ -4,7 +4,7 @@ To enable the Authentiq OmniAuth provider for passwordless authentication you mu
Authentiq will generate a Client ID and the accompanying Client Secret for you to use.
-1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/register).
+1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/developers).
2. On your GitLab server, open the configuration file:
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 425c924cdf2..d22815dfa5e 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -87,9 +87,12 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
encryption: 'plain'
# Enables SSL certificate verification if encryption method is
- # "start_tls" or "simple_tls". (Defaults to false for backward-
- # compatibility)
- verify_certificates: false
+ # "start_tls" or "simple_tls". Defaults to true since GitLab 10.0 for
+ # security. This may break installations upon upgrade to 10.0, that did
+ # not know their LDAP SSL certificates were not setup properly. For
+ # example, when using self-signed certificates, the ca_file path may
+ # need to be specified.
+ verify_certificates: true
# Specifies the path to a file containing a PEM-format CA certificate,
# e.g. if you need to use an internal CA.
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 90a2e9298bf..e09ccaba08c 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -42,6 +42,10 @@ GitLab does not recommend using EFS with GitLab.
are allocated. For smaller volumes, users may experience decent performance
for a period of time due to 'Burst Credits'. Over a period of weeks to months
credits may run out and performance will bottom out.
+- To keep "Burst Credits" available, it may be necessary to provision more space
+ with 'dummy data'. However, this may get expensive.
+- Another option to maintain "Burst Credits" is to use FS Cache on the server so
+ that AWS doesn't always have to go into EFS to access files.
- For larger volumes, allocated IOPS may not be the problem. Workloads where
many small files are written in a serialized manner are not well-suited for EFS.
EBS with an NFS server on top will perform much better.
diff --git a/doc/administration/integration/koding.md b/doc/administration/integration/koding.md
index b95c425842c..67f9f01efb8 100644
--- a/doc/administration/integration/koding.md
+++ b/doc/administration/integration/koding.md
@@ -1,6 +1,10 @@
# Koding & GitLab
-> [Introduced][ce-5909] in GitLab 8.11.
+>**Notes:**
+- **As of GitLab 10.0, the Koding integration is deprecated and will be removed
+ in a future version. The option to configure it is removed from GitLab's admin
+ area.**
+- [Introduced][ce-5909] in GitLab 8.11.
This document will guide you through installing and configuring Koding with
GitLab.
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index b21817c1fd3..652ca9cf454 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -71,6 +71,15 @@ And in Markdown using fenced code blocks:
Alice -> Bob : Go Away
```
+And in reStructuredText using a directive:
+
+```
+.. plantuml::
+
+ Bob -> Alice: hello
+ Alice -> Bob: Go Away
+```
+
The above blocks will be converted to an HTML img tag with source pointing to the
PlantUML instance. If the PlantUML server is correctly configured, this should
render a nice diagram instead of the block:
@@ -94,4 +103,4 @@ Some parameters can be added to the AsciiDoc block definition:
Markdown does not support any parameters and will always use PNG format.
-[ce-8537]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8537 \ No newline at end of file
+[ce-8537]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8537
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 4b8d5c5cc87..76e071dc673 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -9,6 +9,33 @@ documentation](http://docs.gitlab.com/ee/administration/audit_events.html)
System log files are typically plain text in a standard log file format.
This guide talks about how to read and use these system log files.
+## `production_json.log`
+
+This file lives in `/var/log/gitlab/gitlab-rails/production_json.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/production_json.log` for
+installations from source. (When Gitlab is running in an environment
+other than production, the corresponding logfile is shown here.)
+
+It contains a structured log for Rails controller requests received from
+GitLab, thanks to [Lograge](https://github.com/roidrage/lograge/). Note that
+requests from the API [are not yet logged to this
+file](https://gitlab.com/gitlab-org/gitlab-ce/issues/36189).
+
+Each line contains a JSON line that can be ingested by Elasticsearch, Splunk, etc. For example:
+
+```json
+{"method":"GET","path":"/gitlab/gitlab-ce/issues/1234","format":"html","controller":"Projects::IssuesController","action":"show","status":200,"duration":229.03,"view":174.07,"db":13.24,"time":"2017-08-08T20:15:54.821Z","params":{"namespace_id":"gitlab","project_id":"gitlab-ce","id":"1234"},"remote_ip":"18.245.0.1","user_id":1,"username":"admin"}
+```
+
+In this example, you can see this was a GET request for a specific issue. Notice each line also contains performance data:
+
+1. `duration`: the total time taken to retrieve the request
+2. `view`: total time taken inside the Rails views
+3. `db`: total time to retrieve data from the database
+
+In addition, the log contains the IP address from which the request originated
+(`remote_ip`) as well as the user's ID (`user_id`), and username (`username`).
+
## `production.log`
This file lives in `/var/log/gitlab/gitlab-rails/production.log` for
diff --git a/doc/administration/monitoring/performance/performance_bar.md b/doc/administration/monitoring/performance/performance_bar.md
index ee680c7b258..68efe0aae5c 100644
--- a/doc/administration/monitoring/performance/performance_bar.md
+++ b/doc/administration/monitoring/performance/performance_bar.md
@@ -5,17 +5,17 @@ activated, it looks as follows:
![Performance Bar](img/performance_bar.png)
-It allows you to:
+It allows you to see (from left to right):
-- see the current host serving the page
-- see the timing of the page (backend, frontend)
-- the number of DB queries, the time it took, and the detail of these queries
+- the current host serving the page
+- the timing of the page (backend, frontend)
+- time taken and number of DB queries, click through for details of these queries
![SQL profiling using the Performance Bar](img/performance_bar_sql_queries.png)
-- the number of calls to Redis, and the time it took
-- the number of background jobs created by Sidekiq, and the time it took
-- the number of Ruby GC calls, and the time it took
-- profile the code used to generate the page, line by line
+- time taken and number of calls to Redis
+- time taken and number of background jobs created by Sidekiq
+- profile of the code used to generate the page, line by line for either _all_, _app & lib_ , or _views_. In the profile view, the numbers in the left panel represent wall time, cpu time, and number of calls (based on [rblineprof](https://github.com/tmm1/rblineprof)).
![Line profiling using the Performance Bar](img/performance_bar_line_profiling.png)
+- time taken and number of Ruby GC calls
## Enable the Performance Bar via the Admin panel
diff --git a/doc/api/README.md b/doc/api/README.md
index 8acb2145f1a..6cbea29bda6 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -55,27 +55,14 @@ following locations:
- [Tags](tags.md)
- [Todos](todos.md)
- [Users](users.md)
-- [Validate CI configuration](ci/lint.md)
+- [Validate CI configuration](lint.md)
- [V3 to V4](v3_to_v4.md)
- [Version](version.md)
-
-The following documentation is for the [internal CI API](ci/README.md):
-
-- [Builds](ci/builds.md)
-- [Runners](ci/runners.md)
+- [Wikis](wikis.md)
## Road to GraphQL
-Going forward, we will start on moving to
-[GraphQL](http://graphql.org/learn/best-practices/) and deprecate the use of
-controller-specific endpoints. GraphQL has a number of benefits:
-
-1. We avoid having to maintain two different APIs.
-2. Callers of the API can request only what they need.
-3. It is versioned by default.
-
-It will co-exist with the current v4 REST API. If we have a v5 API, this should
-be a compatibility layer on top of GraphQL.
+We have changed our plans to move to GraphQL. After reviewing the GraphQL license, anything related to the Facebook BSD plus patent license will not be allowed at GitLab.
## Basic usage
@@ -251,8 +238,8 @@ The following table gives an overview of how the API functions generally behave.
| ------------ | ----------- |
| `GET` | Access one or more resources and return the result as JSON. |
| `POST` | Return `201 Created` if the resource is successfully created and return the newly created resource as JSON. |
-| `GET` / `PUT` / `DELETE` | Return `200 OK` if the resource is accessed, modified or deleted successfully. The (modified) result is returned as JSON. |
-| `DELETE` | Designed to be idempotent, meaning a request to a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind this, is that the user is not really interested if the resource existed before or not. |
+| `GET` / `PUT` | Return `200 OK` if the resource is accessed or modified successfully. The (modified) result is returned as JSON. |
+| `DELETE` | Returns `204 No Content` if the resuource was deleted successfully. |
The following table shows the possible return codes for API requests.
@@ -268,6 +255,7 @@ The following table shows the possible return codes for API requests.
| `404 Not Found` | A resource could not be accessed, e.g., an ID for a resource could not be found. |
| `405 Method Not Allowed` | The request is not supported. |
| `409 Conflict` | A conflicting resource already exists, e.g., creating a project with a name that already exists. |
+| `412` | Indicates the request was denied. May happen if the `If-Unmodified-Since` header is provided when trying to delete a resource, which was modified in between. |
| `422 Unprocessable` | The entity could not be processed. |
| `500 Server Error` | While handling the request something went wrong server-side. |
diff --git a/doc/api/ci/README.md b/doc/api/ci/README.md
deleted file mode 100644
index 96a281e27c8..00000000000
--- a/doc/api/ci/README.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# GitLab CI API
-
-## Purpose
-
-The main purpose of GitLab CI API is to provide the necessary data and context
-for GitLab CI Runners.
-
-All relevant information about the consumer API can be found in a
-[separate document](../../api/README.md).
-
-## API Prefix
-
-The current CI API prefix is `/ci/api/v1`.
-
-You need to prepend this prefix to all examples in this documentation, like:
-
-```bash
-GET /ci/api/v1/builds/:id/artifacts
-```
-
-## Resources
-
-- [Builds](builds.md)
-- [Runners](runners.md)
diff --git a/doc/api/ci/builds.md b/doc/api/ci/builds.md
deleted file mode 100644
index c8374d94716..00000000000
--- a/doc/api/ci/builds.md
+++ /dev/null
@@ -1,147 +0,0 @@
-# Builds API
-
-API used by runners to receive and update builds.
-
->**Note:**
-This API is intended to be used only by Runners as their own
-communication channel. For the consumer API see the
-[Jobs API](../jobs.md).
-
-## Authentication
-
-This API uses two types of authentication:
-
-1. Unique Runner's token which is the token assigned to the Runner after it
- has been registered.
-
-2. Using the build authorization token.
- This is project's CI token that can be found under the **Builds** section of
- a project's settings. The build authorization token can be passed as a
- parameter or a value of `BUILD-TOKEN` header.
-
-These two methods of authentication are interchangeable.
-
-## Builds
-
-### Runs oldest pending build by runner
-
-```
-POST /ci/api/v1/builds/register
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|---------------------|
-| `token` | string | yes | Unique runner token |
-
-
-```
-curl --request POST "https://gitlab.example.com/ci/api/v1/builds/register" --form "token=t0k3n"
-```
-
-**Responses:**
-
-| Status | Data |Description |
-|--------|------|---------------------------------------------------------------------------|
-| `201` | yes | When a build is scheduled for a runner |
-| `204` | no | When no builds are scheduled for a runner (for GitLab Runner >= `v1.3.0`) |
-| `403` | no | When invalid token is used or no token is sent |
-| `404` | no | When no builds are scheduled for a runner (for GitLab Runner < `v1.3.0`) **or** when the runner is set to `paused` in GitLab runner's configuration page |
-
-### Update details of an existing build
-
-```
-PUT /ci/api/v1/builds/:id
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|----------------------|
-| `id` | integer | yes | The ID of a project |
-| `token` | string | yes | Unique runner token |
-| `state` | string | no | The state of a build |
-| `trace` | string | no | The trace of a build |
-
-```
-curl --request PUT "https://gitlab.example.com/ci/api/v1/builds/1234" --form "token=t0k3n" --form "state=running" --form "trace=Running git clone...\n"
-```
-
-### Incremental build trace update
-
-Using this method you need to send trace content as a request body. You also need to provide the `Content-Range` header
-with a range of sent trace part. Note that you need to send parts in the proper order, so the begining of the part
-must start just after the end of the previous part. If you provide the wrong part, then GitLab CI API will return `416
-Range Not Satisfiable` response with a header `Range: 0-X`, where `X` is the current trace length.
-
-For example, if you receive `Range: 0-11` in the response, then your next part must contain a `Content-Range: 11-...`
-header and a trace part covered by this range.
-
-For a valid update API will return `202` response with:
-* `Build-Status: {status}` header containing current status of the build,
-* `Range: 0-{length}` header with the current trace length.
-
-```
-PATCH /ci/api/v1/builds/:id/trace.txt
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|----------------------|
-| `id` | integer | yes | The ID of a build |
-
-Headers:
-
-| Attribute | Type | Required | Description |
-|-----------------|---------|----------|-----------------------------------|
-| `BUILD-TOKEN` | string | yes | The build authorization token |
-| `Content-Range` | string | yes | Bytes range of trace that is sent |
-
-```
-curl --request PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" --header "BUILD-TOKEN=build_t0k3n" --header "Content-Range=0-21" --data "Running git clone...\n"
-```
-
-
-### Upload artifacts to build
-
-```
-POST /ci/api/v1/builds/:id/artifacts
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|-------------------------------|
-| `id` | integer | yes | The ID of a build |
-| `token` | string | yes | The build authorization token |
-| `file` | mixed | yes | Artifacts file |
-
-```
-curl --request POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n" --form "file=@/path/to/file"
-```
-
-### Download the artifacts file from build
-
-```
-GET /ci/api/v1/builds/:id/artifacts
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|-------------------------------|
-| `id` | integer | yes | The ID of a build |
-| `token` | string | yes | The build authorization token |
-
-```
-curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n"
-```
-
-### Remove the artifacts file from build
-
-```
-DELETE /ci/api/v1/builds/:id/artifacts
-```
-
-| Attribute | Type | Required | Description |
-|-----------|---------|----------|-------------------------------|
-| ` id` | integer | yes | The ID of a build |
-| `token` | string | yes | The build authorization token |
-
-```
-curl --request DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n"
-```
diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md
deleted file mode 100644
index e4a6dc809b1..00000000000
--- a/doc/api/ci/lint.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# Validate the .gitlab-ci.yml (API)
-
-> [Introduced][ce-5953] in GitLab 8.12.
-
-Checks if your .gitlab-ci.yml file is valid.
-
-```
-POST ci/lint
-```
-
-| Attribute | Type | Required | Description |
-| ---------- | ------- | -------- | -------- |
-| `content` | string | yes | the .gitlab-ci.yaml content|
-
-```bash
-curl --header "Content-Type: application/json" https://gitlab.example.com/api/v4/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}'
-```
-
-Be sure to copy paste the exact contents of `.gitlab-ci.yml` as YAML is very picky about indentation and spaces.
-
-Example responses:
-
-* Valid content:
-
- ```json
- {
- "status": "valid",
- "errors": []
- }
- ```
-
-* Invalid content:
-
- ```json
- {
- "status": "invalid",
- "errors": [
- "variables config should be a hash of key value pairs"
- ]
- }
- ```
-
-* Without the content attribute:
-
- ```json
- {
- "error": "content is missing"
- }
- ```
-
-[ce-5953]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5953
diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md
deleted file mode 100644
index 342c039dad8..00000000000
--- a/doc/api/ci/runners.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# Register and Delete Runners API
-
-API used by Runners to register and delete themselves.
-
->**Note:**
-This API is intended to be used only by Runners as their own
-communication channel. For the consumer API see the
-[new Runners API](../runners.md).
-
-## Authentication
-
-This API uses two types of authentication:
-
-1. Unique Runner's token, which is the token assigned to the Runner after it
- has been registered. This token can be found on the Runner's edit page (go to
- **Project > Runners**, select one of the Runners listed under **Runners activated for
- this project**).
-
-2. Using Runners' registration token.
- This is a token that can be found in project's settings.
- It can also be found in the **Admin > Runners** settings area.
- There are two types of tokens you can pass: shared Runner registration
- token or project specific registration token.
-
-## Register a new runner
-
-Used to make GitLab CI aware of available runners.
-
-```sh
-POST /ci/api/v1/runners/register
-```
-
-| Attribute | Type | Required | Description |
-| --------- | ------- | --------- | ----------- |
-| `token` | string | yes | Runner's registration token |
-
-Example request:
-
-```sh
-curl --request POST "https://gitlab.example.com/ci/api/v1/runners/register" --form "token=t0k3n"
-```
-
-## Delete a Runner
-
-Used to remove a Runner.
-
-```sh
-DELETE /ci/api/v1/runners/delete
-```
-
-| Attribute | Type | Required | Description |
-| --------- | ------- | --------- | ----------- |
-| `token` | string | yes | Unique Runner's token |
-
-Example request:
-
-```sh
-curl --request DELETE "https://gitlab.example.com/ci/api/v1/runners/delete" --form "token=t0k3n"
-```
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index 4fa800ecb9c..273d5a56b6f 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -106,7 +106,7 @@ Example response:
Creates a new deploy key for a project.
If the deploy key already exists in another project, it will be joined to current
-project only if original one was is accessible by the same user.
+project only if original one is accessible by the same user.
```
POST /projects/:id/deploy_keys
diff --git a/doc/api/environments.md b/doc/api/environments.md
index 5ca766bf87d..e8deb3e07e9 100644
--- a/doc/api/environments.md
+++ b/doc/api/environments.md
@@ -94,7 +94,7 @@ Example response:
## Delete an environment
-It returns `200` if the environment was successfully deleted, and `404` if the environment does not exist.
+It returns `204` if the environment was successfully deleted, and `404` if the environment does not exist.
```
DELETE /projects/:id/environments/:environment_id
diff --git a/doc/api/events.md b/doc/api/events.md
index 3d5170f3f1e..129af0afa35 100644
--- a/doc/api/events.md
+++ b/doc/api/events.md
@@ -79,7 +79,6 @@ Example response:
"target_id":160,
"target_type":"Issue",
"author_id":25,
- "data":null,
"target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
"created_at":"2017-02-09T10:43:19.667Z",
"author":{
@@ -99,7 +98,6 @@ Example response:
"target_id":159,
"target_type":"Issue",
"author_id":21,
- "data":null,
"target_title":"Nostrum enim non et sed optio illo deleniti non.",
"created_at":"2017-02-09T10:43:19.426Z",
"author":{
@@ -151,7 +149,6 @@ Example response:
"target_id": 830,
"target_type": "Issue",
"author_id": 1,
- "data": null,
"target_title": "Public project search field",
"author": {
"name": "Dmitriy Zaporozhets",
@@ -166,7 +163,7 @@ Example response:
{
"title": null,
"project_id": 15,
- "action_name": "opened",
+ "action_name": "pushed",
"target_id": null,
"target_type": null,
"author_id": 1,
@@ -179,31 +176,14 @@ Example response:
"web_url": "http://localhost:3000/root"
},
"author_username": "john",
- "data": {
- "before": "50d4420237a9de7be1304607147aec22e4a14af7",
- "after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "ref": "refs/heads/master",
- "user_id": 1,
- "user_name": "Dmitriy Zaporozhets",
- "repository": {
- "name": "gitlabhq",
- "url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
- "description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
- "homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
- },
- "commits": [
- {
- "id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "message": "Add simple search to projects in public area",
- "timestamp": "2013-05-13T18:18:08+00:00",
- "url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
- "author": {
- "name": "Dmitriy Zaporozhets",
- "email": "dmitriy.zaporozhets@gmail.com"
- }
- }
- ],
- "total_commits_count": 1
+ "push_data": {
+ "commit_count": 1,
+ "action": "pushed",
+ "ref_type": "branch",
+ "commit_from": "50d4420237a9de7be1304607147aec22e4a14af7",
+ "commit_to": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ "ref": "master",
+ "commit_title": "Add simple search to projects in public area"
},
"target_title": null
},
@@ -214,7 +194,6 @@ Example response:
"target_id": 840,
"target_type": "Issue",
"author_id": 1,
- "data": null,
"target_title": "Finish & merge Code search PR",
"author": {
"name": "Dmitriy Zaporozhets",
@@ -233,7 +212,6 @@ Example response:
"target_id": 1312,
"target_type": "Note",
"author_id": 1,
- "data": null,
"target_title": null,
"created_at": "2015-12-04T10:33:58.089Z",
"note": {
@@ -305,7 +283,6 @@ Example response:
"target_iid":160,
"target_type":"Issue",
"author_id":25,
- "data":null,
"target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
"created_at":"2017-02-09T10:43:19.667Z",
"author":{
@@ -326,7 +303,6 @@ Example response:
"target_iid":159,
"target_type":"Issue",
"author_id":21,
- "data":null,
"target_title":"Nostrum enim non et sed optio illo deleniti non.",
"created_at":"2017-02-09T10:43:19.426Z",
"author":{
diff --git a/doc/api/group_level_variables.md b/doc/api/group_level_variables.md
index e19be7b35c4..33c6da08018 100644
--- a/doc/api/group_level_variables.md
+++ b/doc/api/group_level_variables.md
@@ -1,5 +1,7 @@
# Group-level Variables API
+> [Introduced][ce-34519] in GitLab 9.5
+
## List group variables
Get list of a group's variables.
@@ -123,3 +125,5 @@ DELETE /groups/:id/variables/:key
```
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/groups/1/variables/VARIABLE_1"
```
+
+[ce-34519]: https://gitlab.com/gitlab-org/gitlab-ce/issues/34519
diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md
index dbfc7529125..a96fb3124fc 100644
--- a/doc/api/group_milestones.md
+++ b/doc/api/group_milestones.md
@@ -1,5 +1,8 @@
# Group milestones API
+> **Notes:**
+> [Introduced][ce-12819] in GitLab 9.5.
+
## List group milestones
Returns a list of group milestones.
@@ -118,3 +121,5 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
- `milestone_id` (required) - The ID of a group milestone
+
+[ce-12819]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12819
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 2b3d8e125c8..c2daa8bc029 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -2,7 +2,8 @@
## List groups
-Get a list of groups. (As user: my groups or all available, as admin: all groups).
+Get a list of visible groups for the authenticated user. When accessed without
+authentication, only public groups are returned.
Parameters:
@@ -43,7 +44,8 @@ You can search for groups by name or path, see below.
## List a group's projects
-Get a list of projects in this group.
+Get a list of projects in this group. When accessed without authentication, only
+public projects are returned.
```
GET /groups/:id/projects
@@ -109,7 +111,8 @@ Example response:
## Details of a group
-Get all details of a group.
+Get all details of a group. This endpoint can be accessed without authentication
+if the group is publicly accessible.
```
GET /groups/:id
diff --git a/doc/api/issues.md b/doc/api/issues.md
index f30ed08d0fa..8ca66049d31 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -30,20 +30,22 @@ GET /issues?milestone=1.0.0&state=opened
GET /issues?iids[]=42&iids[]=43
GET /issues?author_id=5
GET /issues?assignee_id=5
-```
-
-| Attribute | Type | Required | Description |
-|-------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------------------|
-| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
-| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
-| `milestone` | string | no | The milestone title |
-| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`. Defaults to `created-by-me` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
-| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
-| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
-| `search` | string | no | Search issues against their `title` and `description` |
+GET /issues?my_reaction_emoji=star
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
+| `milestone` | string | no | The milestone title |
+| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`. Defaults to `created-by-me` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
+| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Search issues against their `title` and `description` |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/issues
@@ -101,6 +103,12 @@ Example response:
"user_notes_count": 1,
"due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/6",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
"confidential": false
}
]
@@ -125,21 +133,23 @@ GET /groups/:id/issues?iids[]=42&iids[]=43
GET /groups/:id/issues?search=issue+title+or+description
GET /groups/:id/issues?author_id=5
GET /groups/:id/issues?assignee_id=5
+GET /groups/:id/issues?my_reaction_emoji=star
```
-| Attribute | Type | Required | Description |
-|-------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------------------|
-| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
-| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
-| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
-| `milestone` | string | no | The milestone title |
-| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
-| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
-| `search` | string | no | Search group issues against their `title` and `description` |
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
+| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
+| `milestone` | string | no | The milestone title |
+| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Search group issues against their `title` and `description` |
```bash
@@ -198,6 +208,12 @@ Example response:
"user_notes_count": 1,
"due_date": null,
"web_url": "http://example.com/example/example/issues/1",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
"confidential": false
}
]
@@ -222,23 +238,25 @@ GET /projects/:id/issues?iids[]=42&iids[]=43
GET /projects/:id/issues?search=issue+title+or+description
GET /projects/:id/issues?author_id=5
GET /projects/:id/issues?assignee_id=5
-```
-
-| 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 |
-| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` |
-| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
-| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
-| `milestone` | string | no | The milestone title |
-| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
-| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
-| `search` | string | no | Search project issues against their `title` and `description` |
-| `created_after` | datetime | no | Return issues created after the given time (inclusive) |
-| `created_before` | datetime | no | Return issues created before the given time (inclusive) |
+GET /projects/:id/issues?my_reaction_emoji=star
+```
+
+| 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 |
+| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` |
+| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
+| `milestone` | string | no | The milestone title |
+| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
+| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Search project issues against their `title` and `description` |
+| `created_after` | datetime | no | Return issues created after the given time (inclusive) |
+| `created_before` | datetime | no | Return issues created before the given time (inclusive) |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues
@@ -296,6 +314,12 @@ Example response:
"user_notes_count": 1,
"due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/1",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
"confidential": false
}
]
@@ -372,6 +396,12 @@ Example response:
"user_notes_count": 1,
"due_date": null,
"web_url": "http://example.com/example/example/issues/1",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
"confidential": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
@@ -440,6 +470,12 @@ Example response:
"user_notes_count": 0,
"due_date": null,
"web_url": "http://example.com/example/example/issues/14",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
"confidential": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
@@ -509,6 +545,12 @@ Example response:
"user_notes_count": 0,
"due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/15",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
"confidential": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
@@ -558,7 +600,7 @@ POST /projects/:id/issues/:issue_iid/move
| `to_project_id` | integer | yes | The ID of the new project |
```bash
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85/move
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"to_project_id": 5}' https://gitlab.example.com/api/v4/projects/4/issues/85/move
```
Example response:
@@ -601,6 +643,12 @@ Example response:
},
"due_date": null,
"web_url": "http://example.com/example/example/issues/11",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
"confidential": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
@@ -672,6 +720,12 @@ Example response:
},
"due_date": null,
"web_url": "http://example.com/example/example/issues/11",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
"confidential": false,
"_links": {
"self": "http://example.com/api/v4/projects/1/issues/2",
@@ -1001,7 +1055,13 @@ Example response:
"user_notes_count": 1,
"should_remove_source_branch": null,
"force_remove_source_branch": false,
- "web_url": "https://gitlab.example.com/gitlab-org/gitlab-test/merge_requests/6432"
+ "web_url": "https://gitlab.example.com/gitlab-org/gitlab-test/merge_requests/6432",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ }
}
]
```
@@ -1039,3 +1099,4 @@ Example response:
```
[ce-13004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13004
+[ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 297115e94ac..d60c7c12881 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -320,11 +320,11 @@ Response:
[ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893
-## Download the artifacts file
+## Download the artifacts archive
> [Introduced][ce-5347] in GitLab 8.10.
-Download the artifacts file from the given reference name and job provided the
+Download the artifacts archive from the given reference name and job provided the
job finished successfully.
```
@@ -354,6 +354,40 @@ Example response:
[ce-5347]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5347
+## Download a single artifact file
+
+> Introduced in GitLab 10.0
+
+Download a single artifact file from within the job's artifacts archive.
+
+Only a single file is going to be extracted from the archive and streamed to a client.
+
+```
+GET /projects/:id/jobs/:job_id/artifacts/*artifact_path
+```
+
+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 |
+| `job_id ` | integer | yes | The unique job identifier |
+| `artifact_path` | string | yes | Path to a file inside the artifacts archive |
+
+Example request:
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/5/artifacts/some/release/file.pdf"
+```
+
+Example response:
+
+| Status | Description |
+|-----------|--------------------------------------|
+| 200 | Sends a single artifact file |
+| 400 | Invalid path provided |
+| 404 | Build not found or no file/artifacts |
+
## Get a trace file
Get a trace of a specific job of a project
diff --git a/doc/api/keys.md b/doc/api/keys.md
index 376ac27df3a..ddcf7830621 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -32,6 +32,7 @@ Parameters:
"twitter": "",
"website_url": "",
"email": "john@example.com",
+ "theme_id": 2,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": null,
diff --git a/doc/api/lint.md b/doc/api/lint.md
new file mode 100644
index 00000000000..bd5a216a99d
--- /dev/null
+++ b/doc/api/lint.md
@@ -0,0 +1,52 @@
+# Validate the .gitlab-ci.yml (API)
+
+> [Introduced][ce-5953] in GitLab 8.12.
+
+Checks if your .gitlab-ci.yml file is valid.
+
+```
+POST /lint
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ------- | -------- | -------- |
+| `content` | string | yes | the .gitlab-ci.yaml content|
+
+```bash
+curl --header "Content-Type: application/json" https://gitlab.example.com/api/v4/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}'
+```
+
+Be sure to copy paste the exact contents of `.gitlab-ci.yml` as YAML is very picky about indentation and spaces.
+
+Example responses:
+
+* Valid content:
+
+ ```json
+ {
+ "status": "valid",
+ "errors": []
+ }
+ ```
+
+* Invalid content:
+
+ ```json
+ {
+ "status": "invalid",
+ "errors": [
+ "variables config should be a hash of key value pairs"
+ ]
+ }
+ ```
+
+* Without the content attribute:
+
+ ```json
+ {
+ "error": "content is missing"
+ }
+ ```
+
+[ce-5953]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5953
+
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 802e5362d70..bff8a2d3e4d 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -22,24 +22,26 @@ GET /merge_requests?state=all
GET /merge_requests?milestone=release
GET /merge_requests?labels=bug,reproduced
GET /merge_requests?author_id=5
+GET /merge_requests?my_reaction_emoji=star
GET /merge_requests?scope=assigned-to-me
```
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, or `merged`|
-| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
-| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
-| `milestone` | string | no | Return merge requests for a specific milestone |
-| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
-| `labels` | string | no | Return merge requests matching a comma separated list of labels |
-| `created_after` | datetime | no | Return merge requests created after the given time (inclusive) |
-| `created_before` | datetime | no | Return merge requests created before the given time (inclusive) |
-| `scope` | string | no | Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`. Defaults to `created-by-me` |
-| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me` |
-| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` |
+| Attribute | Type | Required | Description |
+| ------------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, or `merged` |
+| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
+| `milestone` | string | no | Return merge requests for a specific milestone |
+| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
+| `labels` | string | no | Return merge requests matching a comma separated list of labels |
+| `created_after` | datetime | no | Return merge requests created after the given time (inclusive) |
+| `created_before` | datetime | no | Return merge requests created before the given time (inclusive) |
+| `scope` | string | no | Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`. Defaults to `created-by-me` |
+| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me` |
+| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
```json
[
@@ -92,7 +94,13 @@ Parameters:
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
- "web_url": "http://example.com/example/example/merge_requests/1"
+ "web_url": "http://example.com/example/example/merge_requests/1",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ }
}
]
```
@@ -110,25 +118,27 @@ GET /projects/:id/merge_requests?state=all
GET /projects/:id/merge_requests?iids[]=42&iids[]=43
GET /projects/:id/merge_requests?milestone=release
GET /projects/:id/merge_requests?labels=bug,reproduced
+GET /projects/:id/merge_requests?my_reaction_emoji=star
```
Parameters:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `iids[]` | Array[integer] | no | Return the request having the given `iid` |
-| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, or `merged`|
-| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
-| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
-| `milestone` | string | no | Return merge requests for a specific milestone |
-| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
-| `labels` | string | no | Return merge requests matching a comma separated list of labels |
-| `created_after` | datetime | no | Return merge requests created after the given time (inclusive) |
-| `created_before` | datetime | no | Return merge requests created before the given time (inclusive) |
-| `scope` | string | no | Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13060] in GitLab 9.5)_ |
-| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
-| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
+| `id` | integer | yes | The ID of a project |
+| `iids[]` | Array[integer] | no | Return the request having the given `iid` |
+| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, or `merged` |
+| `order_by` | string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
+| `milestone` | string | no | Return merge requests for a specific milestone |
+| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
+| `labels` | string | no | Return merge requests matching a comma separated list of labels |
+| `created_after` | datetime | no | Return merge requests created after the given time (inclusive) |
+| `created_before` | datetime | no | Return merge requests created before the given time (inclusive) |
+| `scope` | string | no | Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
```json
[
@@ -181,7 +191,13 @@ Parameters:
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
- "web_url": "http://example.com/example/example/merge_requests/1"
+ "web_url": "http://example.com/example/example/merge_requests/1",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ }
}
]
```
@@ -250,7 +266,13 @@ Parameters:
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
- "web_url": "http://example.com/example/example/merge_requests/1"
+ "web_url": "http://example.com/example/example/merge_requests/1",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ }
}
```
@@ -356,6 +378,12 @@ Parameters:
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://example.com/example/example/merge_requests/1",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ }
"changes": [
{
"old_path": "VERSION",
@@ -442,7 +470,13 @@ POST /projects/:id/merge_requests
"user_notes_count": 0,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
- "web_url": "http://example.com/example/example/merge_requests/1"
+ "web_url": "http://example.com/example/example/merge_requests/1",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ }
}
```
@@ -519,7 +553,13 @@ Must include at least one non-required attribute from above.
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
- "web_url": "http://example.com/example/example/merge_requests/1"
+ "web_url": "http://example.com/example/example/merge_requests/1",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ }
}
```
@@ -617,7 +657,13 @@ Parameters:
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
- "web_url": "http://example.com/example/example/merge_requests/1"
+ "web_url": "http://example.com/example/example/merge_requests/1",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ }
}
```
@@ -687,7 +733,13 @@ Parameters:
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
- "web_url": "http://example.com/example/example/merge_requests/1"
+ "web_url": "http://example.com/example/example/merge_requests/1",
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ }
}
```
@@ -1267,3 +1319,4 @@ Example response:
```
[ce-13060]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13060
+[ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index 8133251dffe..5c0bebbaeb0 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -28,12 +28,14 @@ Example response:
[
{
"id": 1,
+ "name": "user1",
"path": "user1",
"kind": "user",
"full_path": "user1"
},
{
"id": 2,
+ "name": "group1",
"path": "group1",
"kind": "group",
"full_path": "group1",
@@ -42,6 +44,7 @@ Example response:
},
{
"id": 3,
+ "name": "bar",
"path": "bar",
"kind": "group",
"full_path": "foo/bar",
@@ -77,6 +80,7 @@ Example response:
[
{
"id": 4,
+ "name": "twitter",
"path": "twitter",
"kind": "group",
"full_path": "twitter",
diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md
index 433654c18cc..c28f48e5fc6 100644
--- a/doc/api/pipeline_schedules.md
+++ b/doc/api/pipeline_schedules.md
@@ -84,7 +84,13 @@ curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/root"
- }
+ },
+ "variables": [
+ {
+ "key": "TEST_VARIABLE_1",
+ "value": "TEST_1"
+ }
+ ]
}
```
@@ -271,3 +277,86 @@ curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gi
}
}
```
+
+## Pipeline schedule variable
+
+> [Introduced][ce-34518] in GitLab 10.0.
+
+## Create a new pipeline schedule variable
+
+Create a new variable of a pipeline schedule.
+
+```
+POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables
+```
+
+| 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 |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
+| `value` | string | yes | The `value` of a variable |
+
+```sh
+curl --request POST --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form "key=NEW_VARIABLE" --form "value=new value" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/variables"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "new value"
+}
+```
+
+## Edit a pipeline schedule variable
+
+Updates the variable of a pipeline schedule.
+
+```
+PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key
+```
+
+| 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 |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+| `key` | string | yes | The `key` of a variable |
+| `value` | string | yes | The `value` of a variable |
+
+```sh
+curl --request PUT --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form "value=updated value" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/variables/NEW_VARIABLE"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "updated value"
+}
+```
+
+## Delete a pipeline schedule variable
+
+Delete the variable of a pipeline schedule.
+
+```
+DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key
+```
+
+| 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 |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+| `key` | string | yes | The `key` of a variable |
+
+```sh
+curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/variables/NEW_VARIABLE"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "updated value"
+}
+```
+
+[ce-34518]: https://gitlab.com/gitlab-org/gitlab-ce/issues/34518 \ No newline at end of file
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index d74398c6e65..ad2521230e6 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -95,8 +95,7 @@ Parameters:
## Delete snippet
-Deletes an existing project snippet. This is an idempotent function and deleting a non-existent
-snippet still returns a `200 OK` status code.
+Deletes an existing project snippet. This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
```
DELETE /projects/:id/snippets/:snippet_id
@@ -150,4 +149,4 @@ Example response:
}
```
-[ce-[ce-29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508
+[ce-29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508
diff --git a/doc/api/projects.md b/doc/api/projects.md
index d3f8e509612..3144220e588 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1,6 +1,6 @@
# Projects API
-### Project visibility level
+## Project visibility level
Project in GitLab can be either private, internal or public.
This is determined by the `visibility` field in the project.
@@ -16,16 +16,15 @@ Values for the project visibility level are:
* `public`:
The project can be cloned without any authentication.
-## List projects
+## List all projects
-Get a list of visible projects for authenticated user. When accessed without authentication, only public projects are returned.
+Get a list of all visible projects across GitLab for the authenticated user.
+When accessed without authentication, only public projects are returned.
```
GET /projects
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `archived` | boolean | no | Limit by archived status |
@@ -70,6 +69,7 @@ Parameters:
"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",
@@ -137,6 +137,7 @@ Parameters:
"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",
@@ -191,16 +192,15 @@ Parameters:
]
```
-### List a user's projects
+## List user projects
-Get a list of visible projects for the given user. When accessed without authentication, only public projects are returned.
+Get a list of visible projects for the given user. When accessed without
+authentication, only public projects are returned.
```
GET /users/:user_id/projects
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `user_id` | string | yes | The ID or username of the user |
@@ -246,6 +246,7 @@ Parameters:
"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",
@@ -313,6 +314,7 @@ Parameters:
"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",
@@ -367,7 +369,7 @@ Parameters:
]
```
-### Get single project
+## Get single project
Get a specific project. This endpoint can be accessed without authentication if
the project is publicly accessible.
@@ -376,8 +378,6 @@ the project is publicly accessible.
GET /projects/:id
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -411,6 +411,7 @@ Parameters:
"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",
@@ -480,17 +481,14 @@ Parameters:
Get the users list of a project.
-
-Parameters:
+```
+GET /projects/:id/users
+```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `search` | string | no | Search for specific users |
-```
-GET /projects/:id/users
-```
-
```json
[
{
@@ -512,11 +510,11 @@ GET /projects/:id/users
]
```
-### Get project events
+## Get project events
-Please refer to the [Events API documentation](events.md#list-a-projects-visible-events)
+Please refer to the [Events API documentation](events.md#list-a-projects-visible-events).
-### Create project
+## Create project
Creates a new project owned by the authenticated user.
@@ -524,8 +522,6 @@ Creates a new project owned by the authenticated user.
POST /projects
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `name` | string | yes if path is not provided | The name of the new project. Equals path if not provided. |
@@ -537,6 +533,7 @@ Parameters:
| `jobs_enabled` | boolean | no | Enable jobs for this project |
| `wiki_enabled` | boolean | no | Enable wiki for this project |
| `snippets_enabled` | boolean | no | Enable snippets for this project |
+| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
| `visibility` | string | no | See [project visibility level](#project-visibility-level) |
@@ -551,7 +548,7 @@ Parameters:
| `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line |
| `ci_config_path` | string | no | The path to CI config file |
-### Create project for user
+## Create project for user
Creates a new project owned by the specified user. Available only for admins.
@@ -559,8 +556,6 @@ Creates a new project owned by the specified user. Available only for admins.
POST /projects/user/:user_id
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `user_id` | integer | yes | The user ID of the project owner |
@@ -574,6 +569,7 @@ Parameters:
| `jobs_enabled` | boolean | no | Enable jobs for this project |
| `wiki_enabled` | boolean | no | Enable wiki for this project |
| `snippets_enabled` | boolean | no | Enable snippets for this project |
+| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
| `visibility` | string | no | See [project visibility level](#project-visibility-level) |
@@ -588,7 +584,7 @@ Parameters:
| `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line |
| `ci_config_path` | string | no | The path to CI config file |
-### Edit project
+## Edit project
Updates an existing project.
@@ -596,8 +592,6 @@ Updates an existing project.
PUT /projects/:id
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -610,6 +604,7 @@ Parameters:
| `jobs_enabled` | boolean | no | Enable jobs for this project |
| `wiki_enabled` | boolean | no | Enable wiki for this project |
| `snippets_enabled` | boolean | no | Enable snippets for this project |
+| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
| `visibility` | string | no | See [project visibility level](#project-visibility-level) |
@@ -623,24 +618,24 @@ Parameters:
| `avatar` | mixed | no | Image file for avatar of the project |
| `ci_config_path` | string | no | The path to CI config file |
-### Fork project
+## Fork project
Forks a project into the user namespace of the authenticated user or the one provided.
-The forking operation for a project is asynchronous and is completed in a background job. The request will return immediately. To determine whether the fork of the project has completed, query the `import_status` for the new project.
+The forking operation for a project is asynchronous and is completed in a
+background job. The request will return immediately. To determine whether the
+fork of the project has completed, query the `import_status` for the new project.
```
POST /projects/:id/fork
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `namespace` | integer/string | yes | The ID or path of the namespace that the project will be forked to |
-### Star a project
+## Star a project
Stars a given project. Returns status code `304` if the project is already starred.
@@ -648,8 +643,6 @@ Stars a given project. Returns status code `304` if the project is already starr
POST /projects/:id/star
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -683,6 +676,7 @@ Example response:
"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",
@@ -717,7 +711,7 @@ Example response:
}
```
-### Unstar a project
+## Unstar a project
Unstars a given project. Returns status code `304` if the project is not starred.
@@ -758,6 +752,7 @@ Example response:
"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",
@@ -792,7 +787,7 @@ Example response:
}
```
-### Archive a project
+## Archive a project
Archives the project if the user is either admin or the project owner of this project. This action is
idempotent, thus archiving an already archived project will not change the project.
@@ -839,6 +834,7 @@ Example response:
"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",
@@ -885,7 +881,7 @@ Example response:
}
```
-### Unarchive a project
+## Unarchive a project
Unarchives the project if the user is either admin or the project owner of this project. This action is
idempotent, thus unarchiving an non-archived project will not change the project.
@@ -932,6 +928,7 @@ Example response:
"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",
@@ -978,7 +975,7 @@ Example response:
}
```
-### Remove project
+## Remove project
Removes a project including all associated resources (issues, merge requests etc.)
@@ -986,15 +983,11 @@ Removes a project including all associated resources (issues, merge requests etc
DELETE /projects/:id
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
-## Uploads
-
-### Upload a file
+## Upload a file
Uploads a file to the specified project to be used in an issue or merge request description, or a comment.
@@ -1002,8 +995,6 @@ Uploads a file to the specified project to be used in an issue or merge request
POST /projects/:id/uploads
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -1028,15 +1019,11 @@ Returned object:
}
```
-**Note**: The returned `url` is relative to the project path.
+>**Note**: The returned `url` is relative to the project path.
In Markdown contexts, the link is automatically expanded when the format in
`markdown` is used.
-## Project members
-
-Please consult the [Project Members](members.md) documentation.
-
-### Share project with group
+## Share project with group
Allow to share project with group.
@@ -1044,8 +1031,6 @@ Allow to share project with group.
POST /projects/:id/share
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -1053,7 +1038,7 @@ Parameters:
| `group_access` | integer | yes | The permissions level to grant the group |
| `expires_at` | string | no | Share expiration date in ISO 8601 format: 2016-09-26 |
-### Delete a shared project link within a group
+## Delete a shared project link within a group
Unshare the project from the group. Returns `204` and no content on success.
@@ -1061,8 +1046,6 @@ Unshare the project from the group. Returns `204` and no content on success.
DELETE /projects/:id/share/:group_id
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -1085,8 +1068,6 @@ Get a list of project hooks.
GET /projects/:id/hooks
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -1099,8 +1080,6 @@ Get a specific hook for a project.
GET /projects/:id/hooks/:hook_id
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -1132,8 +1111,6 @@ Adds a hook to a specified project.
POST /projects/:id/hooks
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -1157,8 +1134,6 @@ Edits a hook for a specified project.
PUT /projects/:id/hooks/:hook_id
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -1184,8 +1159,6 @@ Either the hook is available or not.
DELETE /projects/:id/hooks/:hook_id
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -1194,126 +1167,16 @@ Parameters:
Note the JSON response differs if the hook is available or not. If the project hook
is available before it is returned in the JSON response or an empty response is returned.
-## Branches
-
-For more information please consult the [Branches](branches.md) documentation.
-
-### List branches
-
-Lists all branches of a project.
-
-```
-GET /projects/:id/repository/branches
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
-
-```json
-[
- {
- "name": "async",
- "commit": {
- "id": "a2b702edecdf41f07b42653eb1abe30ce98b9fca",
- "parent_ids": [
- "3f94fc7c85061973edc9906ae170cc269b07ca55"
- ],
- "message": "give Caolan credit where it's due (up top)",
- "author_name": "Jeremy Ashkenas",
- "author_email": "jashkenas@example.com",
- "authored_date": "2010-12-08T21:28:50+00:00",
- "committer_name": "Jeremy Ashkenas",
- "committer_email": "jashkenas@example.com",
- "committed_date": "2010-12-08T21:28:50+00:00"
- },
- "protected": false,
- "developers_can_push": false,
- "developers_can_merge": false
- },
- {
- "name": "gh-pages",
- "commit": {
- "id": "101c10a60019fe870d21868835f65c25d64968fc",
- "parent_ids": [
- "9c15d2e26945a665131af5d7b6d30a06ba338aaa"
- ],
- "message": "Underscore.js 1.5.2",
- "author_name": "Jeremy Ashkenas",
- "author_email": "jashkenas@example.com",
- "authored_date": "2013-09-07T12:58:21+00:00",
- "committer_name": "Jeremy Ashkenas",
- "committer_email": "jashkenas@example.com",
- "committed_date": "2013-09-07T12:58:21+00:00"
- },
- "protected": false,
- "developers_can_push": false,
- "developers_can_merge": false
- }
-]
-```
-
-### Single branch
-
-A specific branch of a project.
-
-```
-GET /projects/:id/repository/branches/:branch
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
-| `branch` | string | yes | The name of the branch |
-| `developers_can_push` | boolean | no | Flag if developers can push to the branch |
-| `developers_can_merge` | boolean | no | Flag if developers can merge to the branch |
-
-### Protect single branch
-
-Protects a single branch of a project.
-
-```
-PUT /projects/:id/repository/branches/:branch/protect
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
-| `branch` | string | yes | The name of the branch |
-
-### Unprotect single branch
-
-Unprotects a single branch of a project.
-
-```
-PUT /projects/:id/repository/branches/:branch/unprotect
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
-| `branch` | string | yes | The name of the branch |
-
## Admin fork relation
Allows modification of the forked relationship between existing projects. Available only for admins.
-### Create a forked from/to relation between existing projects.
+### Create a forked from/to relation between existing projects
```
POST /projects/:id/fork/:forked_from_id
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -1325,8 +1188,6 @@ Parameters:
DELETE /projects/:id/fork
```
-Parameter:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
@@ -1341,8 +1202,6 @@ accessible.
GET /projects
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `search` | string | yes | A string contained in the project name |
@@ -1355,14 +1214,20 @@ curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/a
## Start the Housekeeping task for a Project
->**Note:** This feature was introduced in GitLab 9.0
+> Introduced in GitLab 9.0.
```
POST /projects/:id/housekeeping
```
-Parameters:
-
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+
+## Branches
+
+Read more in the [Branches](branches.md) documentation.
+
+## Project members
+
+Read more in the [Project members](members.md) documentation.
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 16d362a3530..6304a496f94 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -138,7 +138,8 @@ Example response:
"ruby",
"mysql"
],
- "version": null
+ "version": null,
+ "access_level": "ref_protected"
}
```
@@ -156,6 +157,9 @@ PUT /runners/:id
| `description` | string | no | The description of a runner |
| `active` | boolean | no | The state of a runner; can be set to `true` or `false` |
| `tag_list` | array | no | The list of tags for a runner; put array of tags, that should be finally assigned to a runner |
+| `run_untagged` | boolean | no | Flag indicating the runner can execute untagged jobs |
+| `locked` | boolean | no | Flag indicating the runner is locked |
+| `access_level` | string | no | The access_level of the runner; `not_protected` or `ref_protected` |
```
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
@@ -190,7 +194,8 @@ Example response:
"tag1",
"tag2"
],
- "version": null
+ "version": null,
+ "access_level": "ref_protected"
}
```
diff --git a/doc/api/session.md b/doc/api/session.md
index f79eac11689..b97e26f34a2 100644
--- a/doc/api/session.md
+++ b/doc/api/session.md
@@ -39,6 +39,7 @@ Example response:
"twitter": "",
"website_url": "",
"email": "john@example.com",
+ "theme_id": 1,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": "2015-07-07T07:10:58.392Z",
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 94a9f8265fb..b78f1252108 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -48,7 +48,11 @@ Example response:
"plantuml_enabled": false,
"plantuml_url": null,
"terminal_max_session_time": 0,
- "polling_interval_multiplier": 1.0
+ "polling_interval_multiplier": 1.0,
+ "rsa_key_restriction": 0,
+ "dsa_key_restriction": 0,
+ "ecdsa_key_restriction": 0,
+ "ed25519_key_restriction": 0,
}
```
@@ -88,6 +92,10 @@ PUT /application/settings
| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. |
| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. |
| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling. |
+| `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys.
+| `dsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded DSA key. Default is `0` (no restriction). `-1` disables DSA keys.
+| `ecdsa_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ECDSA key. Default is `0` (no restriction). `-1` disables ECDSA keys.
+| `ed25519_key_restriction` | integer | no | The minimum allowed curve size (in bits) of an uploaded ED25519 key. Default is `0` (no restriction). `-1` disables ED25519 keys.
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
@@ -125,6 +133,10 @@ Example response:
"plantuml_enabled": false,
"plantuml_url": null,
"terminal_max_session_time": 0,
- "polling_interval_multiplier": 1.0
+ "polling_interval_multiplier": 1.0,
+ "rsa_key_restriction": 0,
+ "dsa_key_restriction": 0,
+ "ecdsa_key_restriction": 0,
+ "ed25519_key_restriction": 0,
}
```
diff --git a/doc/api/users.md b/doc/api/users.md
index 57a13eb477d..6d5db16b36a 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -72,6 +72,7 @@ GET /users
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
@@ -105,6 +106,7 @@ GET /users
"organization": "",
"last_sign_in_at": null,
"confirmed_at": "2012-05-30T16:53:06.148Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 3,
"projects_limit": 100,
@@ -215,6 +217,7 @@ Parameters:
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
@@ -299,10 +302,7 @@ e.g. when renaming the email address to some existing one.
## User deletion
Deletes a user. Available only for administrators.
-This is an idempotent function, calling this function for a non-existent user id
-still returns a status code `200 OK`.
-The JSON response differs if the user was actually deleted or not.
-In the former the user is returned and in the latter not.
+This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
```
DELETE /users/:id
@@ -344,6 +344,7 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
@@ -390,6 +391,7 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
@@ -524,8 +526,7 @@ Parameters:
## Delete SSH key for current user
Deletes key owned by currently authenticated user.
-This is an idempotent function and calling it on a key that is already deleted
-or not available results in `200 OK`.
+This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
```
DELETE /user/keys/:key_id
@@ -548,7 +549,216 @@ Parameters:
- `id` (required) - id of specified user
- `key_id` (required) - SSH key ID
-Will return `200 OK` on success, or `404 Not found` if either user or key cannot be found.
+## List all GPG keys
+
+Get a list of currently authenticated user's GPG keys.
+
+```
+GET /user/gpg_keys
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+]
+```
+
+## Get a specific GPG key
+
+Get a specific GPG key of currently authenticated user.
+
+```
+GET /user/gpg_keys/:key_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys/1
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+```
+
+## Add a GPG key
+
+Creates a new GPG key owned by the currently authenticated user.
+
+```
+POST /user/gpg_keys
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| key | string | yes | The new GPG key |
+
+```bash
+curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+]
+```
+
+## Delete a GPG key
+
+Delete a GPG key owned by currently authenticated user.
+
+```
+DELETE /user/gpg_keys/:key_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/gpg_keys/1
+```
+
+Returns `204 No Content` on success, or `404 Not found` if the key cannot be found.
+
+## List all GPG keys for given user
+
+Get a list of a specified user's GPG keys. Available only for admins.
+
+```
+GET /users/:id/gpg_keys
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+]
+```
+
+## Get a specific GPG key for a given user
+
+Get a specific GPG key for a given user. Available only for admins.
+
+```
+GET /users/:id/gpg_keys/:key_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys/1
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+```
+
+## Add a GPG key for a given user
+
+Create new GPG key owned by the specified user. Available only for admins.
+
+```
+POST /users/:id/gpg_keys
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --data "key=-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFV..." --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "key": "-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n\r\nxsBNBFVjnlIBCACibzXOLCiZiL2oyzYUaTOCkYnSUhymg3pdbfKtd4mpBa58xKBj\r\nt1pTHVpw3Sk03wmzhM/Ndlt1AV2YhLv++83WKr+gAHFYFiCV/tnY8bx3HqvVoy8O\r\nCfxWhw4QZK7+oYzVmJj8ZJm3ZjOC4pzuegNWlNLCUdZDx9OKlHVXLCX1iUbjdYWa\r\nqKV6tdV8hZolkbyjedQgrpvoWyeSHHpwHF7yk4gNJWMMI5rpcssL7i6mMXb/sDzO\r\nVaAtU5wiVducsOa01InRFf7QSTxoAm6Xy0PGv/k48M6xCALa9nY+BzlOv47jUT57\r\nvilf4Szy9dKD0v9S0mQ+IHB+gNukWrnwtXx5ABEBAAHNFm5hbWUgKGNvbW1lbnQp\r\nIDxlbUBpbD7CwHUEEwECACkFAlVjnlIJEINgJNgv009/AhsDAhkBBgsJCAcDAgYV\r\nCAIJCgsEFgIDAQAAxqMIAFBHuBA8P1v8DtHonIK8Lx2qU23t8Mh68HBIkSjk2H7/\r\noO2cDWCw50jZ9D91PXOOyMPvBWV2IE3tARzCvnNGtzEFRtpIEtZ0cuctxeIF1id5\r\ncrfzdMDsmZyRHAOoZ9VtuD6mzj0ybQWMACb7eIHjZDCee3Slh3TVrLy06YRdq2I4\r\nbjMOPePtK5xnIpHGpAXkB3IONxyITpSLKsA4hCeP7gVvm7r7TuQg1ygiUBlWbBYn\r\niE5ROzqZjG1s7dQNZK/riiU2umGqGuwAb2IPvNiyuGR3cIgRE4llXH/rLuUlspAp\r\no4nlxaz65VucmNbN1aMbDXLJVSqR1DuE00vEsL1AItI=\r\n=XQoy\r\n-----END PGP PUBLIC KEY BLOCK-----",
+ "created_at": "2017-09-05T09:17:46.264Z"
+ }
+]
+```
+
+## Delete a GPG key for a given user
+
+Delete a GPG key owned by a specified user. Available only for admins.
+
+```
+DELETE /users/:id/gpg_keys/:key_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the user |
+| `key_id` | integer | yes | The ID of the GPG key |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/2/gpg_keys/1
+```
## List emails
@@ -654,8 +864,7 @@ Parameters:
## Delete email for current user
Deletes email owned by currently authenticated user.
-This is an idempotent function and calling it on a email that is already deleted
-or not available results in `200 OK`.
+This returns a `204 No Content` status code if the operation was successfully or `404` if the resource was not found.
```
DELETE /user/emails/:email_id
@@ -678,8 +887,6 @@ Parameters:
- `id` (required) - id of specified user
- `email_id` (required) - email ID
-Will return `200 OK` on success, or `404 Not found` if either user or email cannot be found.
-
## Block user
Blocks the specified user. Available only for admin.
diff --git a/doc/api/wikis.md b/doc/api/wikis.md
new file mode 100644
index 00000000000..10eebc4a3c1
--- /dev/null
+++ b/doc/api/wikis.md
@@ -0,0 +1,157 @@
+# Wikis API
+
+> [Introduced][ce-13372] in GitLab 10.0.
+
+Available only in APIv4.
+
+## List wiki pages
+
+Get all wiki pages for a given project.
+
+```
+GET /projects/:id/wikis
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `with_content` | boolean | no | Include pages' content |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/wikis?with_content=1
+```
+
+Example response:
+
+```json
+[
+ {
+ "content" : "Here is an instruction how to deploy this project.",
+ "format" : "markdown",
+ "slug" : "deploy",
+ "title" : "deploy"
+ },
+ {
+ "content" : "Our development process is described here.",
+ "format" : "markdown",
+ "slug" : "development",
+ "title" : "development"
+ },{
+ "content" : "* [Deploy](deploy)\n* [Development](development)",
+ "format" : "markdown",
+ "slug" : "home",
+ "title" : "home"
+ }
+]
+```
+
+## Get a wiki page
+
+Get a wiki page for a given project.
+
+```
+GET /projects/:id/wikis/:slug
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `slug` | string | yes | The slug (a unique string) of the wiki page |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/wikis/home
+```
+
+Example response:
+
+```json
+[
+ {
+ "content" : "home page",
+ "format" : "markdown",
+ "slug" : "home",
+ "title" : "home"
+ }
+]
+```
+
+## Create a new wiki page
+
+Creates a new wiki page for the given repository with the given title, slug, and content.
+
+```
+POST /projects/:id/wikis
+```
+
+| Attribute | Type | Required | Description |
+| ------------- | ------- | -------- | ---------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `content` | string | yes | The content of the wiki page |
+| `title` | string | yes | The title of the wiki page |
+| `format` | string | no | The format of the wiki page. Available formats are: `markdown` (default), `rdoc`, and `asciidoc` |
+
+```bash
+curl --data "format=rdoc&title=Hello&content=Hello world" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis"
+```
+
+Example response:
+
+```json
+{
+ "content" : "Hello world",
+ "format" : "markdown",
+ "slug" : "Hello",
+ "title" : "Hello"
+}
+```
+
+## Edit an existing wiki page
+
+Updates an existing wiki page. At least one parameter is required to update the wiki page.
+
+```
+PUT /projects/:id/wikis/:slug
+```
+
+| Attribute | Type | Required | Description |
+| --------------- | ------- | --------------------------------- | ------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `content` | string | yes if `title` is not provided | The content of the wiki page |
+| `title` | string | yes if `content` is not provided | The title of the wiki page |
+| `format` | string | no | The format of the wiki page. Available formats are: `markdown` (default), `rdoc`, and `asciidoc` |
+| `slug` | string | yes | The slug (a unique string) of the wiki page |
+
+
+```bash
+curl --request PUT --data "format=rdoc&content=documentation&title=Docs" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis/foo"
+```
+
+Example response:
+
+```json
+{
+ "content" : "documentation",
+ "format" : "markdown",
+ "slug" : "Docs",
+ "title" : "Docs"
+}
+```
+
+## Delete a wiki page
+
+Deletes a wiki page with a given slug.
+
+```
+DELETE /projects/:id/wikis/:slug
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `slug` | string | yes | The slug (a unique string) of the wiki page |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/wikis/foo"
+```
+
+On success the HTTP status code is `204` and no JSON response is expected.
diff --git a/doc/articles/artifactory_and_gitlab/index.md b/doc/articles/artifactory_and_gitlab/index.md
new file mode 100644
index 00000000000..c64851bad2b
--- /dev/null
+++ b/doc/articles/artifactory_and_gitlab/index.md
@@ -0,0 +1,278 @@
+# How to deploy Maven projects to Artifactory with GitLab CI/CD
+
+> **Article [Type](../../development/writing_documentation.md#types-of-technical-articles):** tutorial ||
+> **Level:** intermediary ||
+> **Author:** [Fabio Busatto](https://gitlab.com/bikebilly) ||
+> **Publication date:** 2017-08-15
+
+## Introduction
+
+In this article, we will show how you can leverage the power of [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/)
+to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory](https://www.jfrog.com/artifactory/), and then use it from another Maven application as a dependency.
+
+You'll create two different projects:
+
+- `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep)
+- `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app)
+
+We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/).
+We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it.
+
+## Create the simple Maven dependency
+
+First of all, you need an application to work with: in this specific case we will
+use a simple one, but it could be any Maven application. This will be the
+dependency you want to package and deploy to Artifactory, in order to be
+available to other projects.
+
+### Prepare the dependency application
+
+For this article you'll use a Maven app that can be cloned from our example
+project:
+
+1. Log in to your GitLab account
+1. Create a new project by selecting **Import project from âž” Repo by URL**
+1. Add the following URL:
+
+ ```
+ https://gitlab.com/gitlab-examples/maven/simple-maven-dep.git
+ ```
+1. Click **Create project**
+
+This application is nothing more than a basic class with a stub for a JUnit based test suite.
+It exposes a method called `hello` that accepts a string as input, and prints a hello message on the screen.
+
+The project structure is really simple, and you should consider these two resources:
+
+- `pom.xml`: project object model (POM) configuration file
+- `src/main/java/com/example/dep/Dep.java`: source of our application
+
+### Configure the Artifactory deployment
+
+The application is ready to use, but you need some additional steps to deploy it to Artifactory:
+
+1. Log in to Artifactory with your user's credentials.
+1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel.
+1. Copy to clipboard the configuration snippet under the **Deploy** paragraph.
+1. Change the `url` value in order to have it configurable via secret variables.
+1. Copy the snippet in the `pom.xml` file for your project, just after the
+ `dependencies` section. The snippet should look like this:
+
+ ```xml
+ <distributionManagement>
+ <repository>
+ <id>central</id>
+ <name>83d43b5afeb5-releases</name>
+ <url>${env.MAVEN_REPO_URL}/libs-release-local</url>
+ </repository>
+ </distributionManagement>
+ ```
+
+Another step you need to do before you can deploy the dependency to Artifactory
+is to configure the authentication data. It is a simple task, but Maven requires
+it to stay in a file called `settings.xml` that has to be in the `.m2` subdirectory
+in the user's homedir.
+
+Since you want to use GitLab Runner to automatically deploy the application, you
+should create the file in the project's home directory and set a command line
+parameter in `.gitlab-ci.yml` to use the custom location instead of the default one:
+
+1. Create a folder called `.m2` in the root of your repository
+1. Create a file called `settings.xml` in the `.m2` folder
+1. Copy the following content into a `settings.xml` file:
+
+ ```xml
+ <settings xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"
+ xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ <servers>
+ <server>
+ <id>central</id>
+ <username>${env.MAVEN_REPO_USER}</username>
+ <password>${env.MAVEN_REPO_PASS}</password>
+ </server>
+ </servers>
+ </settings>
+ ```
+
+ Username and password will be replaced by the correct values using secret variables.
+
+### Configure GitLab CI/CD for `simple-maven-dep`
+
+Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) to automatically build, test and deploy the dependency!
+
+GitLab CI/CD uses a file in the root of the repo, named `.gitlab-ci.yml`, to read the definitions for jobs
+that will be executed by the configured GitLab Runners. You can read more about this file in the [GitLab Documentation](https://docs.gitlab.com/ee/ci/yaml/).
+
+First of all, remember to set up secret variables for your deployment. Navigate to your project's **Settings > CI/CD** page
+and add the following secret variables (replace them with your current values, of course):
+
+- **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL)
+- **MAVEN_REPO_USER**: `gitlab` (your Artifactory username)
+- **MAVEN_REPO_PASS**: `AKCp2WXr3G61Xjz1PLmYa3arm3yfBozPxSta4taP3SeNu2HPXYa7FhNYosnndFNNgoEds8BCS` (your Artifactory Encrypted Password)
+
+Now it's time to define jobs in `.gitlab-ci.yml` and push it to the repo:
+
+```yaml
+image: maven:latest
+
+variables:
+ MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
+ MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
+
+cache:
+ paths:
+ - .m2/repository/
+ - target/
+
+build:
+ stage: build
+ script:
+ - mvn $MAVEN_CLI_OPTS compile
+
+test:
+ stage: test
+ script:
+ - mvn $MAVEN_CLI_OPTS test
+
+deploy:
+ stage: deploy
+ script:
+ - mvn $MAVEN_CLI_OPTS deploy
+ only:
+ - master
+```
+
+GitLab Runner will use the latest [Maven Docker image](https://hub.docker.com/_/maven/), which already contains all the tools and the dependencies you need to manage the project,
+in order to run the jobs.
+
+Environment variables are set to instruct Maven to use the `homedir` of the repo instead of the user's home when searching for configuration and dependencies.
+
+Caching the `.m2/repository folder` (where all the Maven files are stored), and the `target` folder (where our application will be created), is useful for speeding up the process
+by running all Maven phases in a sequential order, therefore, executing `mvn test` will automatically run `mvn compile` if necessary.
+
+Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the application.
+
+Deploy to Artifactory is done as defined by the secret variables we have just set up.
+The deployment occurs only if we're pushing or merging to `master` branch, so that the development versions are tested but not published.
+
+Done! Now you have all the changes in the GitLab repo, and a pipeline has already been started for this commit. In the **Pipelines** tab you can see what's happening.
+If the deployment has been successful, the deploy job log will output:
+
+```
+[INFO] ------------------------------------------------------------------------
+[INFO] BUILD SUCCESS
+[INFO] ------------------------------------------------------------------------
+[INFO] Total time: 1.983 s
+```
+
+>**Note**:
+the `mvn` command downloads a lot of files from the internet, so you'll see a lot of extra activity in the log the first time you run it.
+
+Yay! You did it! Checking in Artifactory will confirm that you have a new artifact available in the `libs-release-local` repo.
+
+## Create the main Maven application
+
+Now that you have the dependency available on Artifactory, it's time to use it!
+Let's see how we can have it as a dependency to our main application.
+
+### Prepare the main application
+
+We'll use again a Maven app that can be cloned from our example project:
+
+1. Create a new project by selecting **Import project from âž” Repo by URL**
+1. Add the following URL:
+
+ ```
+ https://gitlab.com/gitlab-examples/maven/simple-maven-app.git
+ ```
+1. Click **Create project**
+
+This one is a simple app as well. If you look at the `src/main/java/com/example/app/App.java`
+file you can see that it imports the `com.example.dep.Dep` class and calls the `hello` method passing `GitLab` as a parameter.
+
+Since Maven doesn't know how to resolve the dependency, you need to modify the configuration:
+
+1. Go back to Artifactory
+1. Browse the `libs-release-local` repository
+1. Select the `simple-maven-dep-1.0.jar` file
+1. Find the configuration snippet from the **Dependency Declaration** section of the main panel
+1. Copy the snippet in the `dependencies` section of the `pom.xml` file.
+ The snippet should look like this:
+
+ ```xml
+ <dependency>
+ <groupId>com.example.dep</groupId>
+ <artifactId>simple-maven-dep</artifactId>
+ <version>1.0</version>
+ </dependency>
+ ```
+
+### Configure the Artifactory repository location
+
+At this point you defined the dependency for the application, but you still miss where you can find the required files.
+You need to create a `.m2/settings.xml` file as you did for the dependency project, and let Maven know the location using environment variables.
+
+Here is how you can get the content of the file directly from Artifactory:
+
+1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel
+1. Click on **Generate Maven Settings**
+1. Click on **Generate Settings**
+1. Copy to clipboard the configuration file
+1. Save the file as `.m2/settings.xml` in your repo
+
+Now you are ready to use the Artifactory repository to resolve dependencies and use `simple-maven-dep` in your main application!
+
+### Configure GitLab CI/CD for `simple-maven-app`
+
+You need a last step to have everything in place: configure the `.gitlab-ci.yml` file for this project, as you already did for `simple-maven-dep`.
+
+You want to leverage [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/) to automatically build, test and run your awesome application,
+and see if you can get the greeting as expected!
+
+All you need to do is to add the following `.gitlab-ci.yml` to the repo:
+
+```yaml
+image: maven:latest
+
+stages:
+ - build
+ - test
+ - run
+
+variables:
+ MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
+ MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
+
+cache:
+ paths:
+ - .m2/repository/
+ - target/
+
+build:
+ stage: build
+ script:
+ - mvn $MAVEN_CLI_OPTS compile
+
+test:
+ stage: test
+ script:
+ - mvn $MAVEN_CLI_OPTS test
+
+run:
+ stage: run
+ script:
+ - mvn $MAVEN_CLI_OPTS package
+ - mvn $MAVEN_CLI_OPTS exec:java -Dexec.mainClass="com.example.app.App"
+```
+
+It is very similar to the configuration used for `simple-maven-dep`, but instead of the `deploy` job there is a `run` job.
+Probably something that you don't want to use in real projects, but here it is useful to see the application executed automatically.
+
+And that's it! In the `run` job output log you will find a friendly hello to GitLab!
+
+## Conclusion
+
+In this article we covered the basic steps to use an Artifactory Maven repository to automatically publish and consume artifacts.
+
+A similar approach could be used to interact with any other Maven compatible Binary Repository Manager.
+Obviously, you can improve these examples, optimizing the `.gitlab-ci.yml` file to better suit your needs, and adapting to your workflow.
diff --git a/doc/articles/how_to_configure_ldap_gitlab_ce/index.md b/doc/articles/how_to_configure_ldap_gitlab_ce/index.md
index 130e8f542b4..25a24bc1d32 100644
--- a/doc/articles/how_to_configure_ldap_gitlab_ce/index.md
+++ b/doc/articles/how_to_configure_ldap_gitlab_ce/index.md
@@ -3,7 +3,7 @@
> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** admin guide ||
> **Level:** intermediary ||
> **Author:** [Chris Wilson](https://gitlab.com/MrChrisW) ||
-> **Publication date:** 2017/05/03
+> **Publication date:** 2017-05-03
## Introduction
diff --git a/doc/articles/how_to_install_git/index.md b/doc/articles/how_to_install_git/index.md
index 66d866b2d09..37b60501ce2 100644
--- a/doc/articles/how_to_install_git/index.md
+++ b/doc/articles/how_to_install_git/index.md
@@ -3,7 +3,7 @@
> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** user guide ||
> **Level:** beginner ||
> **Author:** [Sean Packham](https://gitlab.com/SeanPackham) ||
-> **Publication date:** 2017/05/15
+> **Publication date:** 2017-05-15
To begin contributing to GitLab projects
you will need to install the Git client on your computer.
diff --git a/doc/articles/index.md b/doc/articles/index.md
index 558c624fe39..798d4cbf4ff 100644
--- a/doc/articles/index.md
+++ b/doc/articles/index.md
@@ -17,8 +17,8 @@ Explore GitLab's supported [authentications methods](../topics/authentication/in
| Article title | Category | Publishing date |
| :------------ | :------: | --------------: |
| **LDAP** |
-| [How to configure LDAP with GitLab CE](how_to_configure_ldap_gitlab_ce/index.md)| Admin guide | 2017/05/03 |
-| [How to configure LDAP with GitLab EE](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/) | Admin guide | 2017/05/03 |
+| [How to configure LDAP with GitLab CE](how_to_configure_ldap_gitlab_ce/index.md)| Admin guide | 2017-05-03 |
+| [How to configure LDAP with GitLab EE](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/) | Admin guide | 2017-05-03 |
## Build, test, and deploy with GitLab CI/CD
@@ -26,17 +26,19 @@ Build, test, and deploy the software you develop with [GitLab CI/CD](../ci/READM
| Article title | Category | Publishing date |
| :------------ | :------: | --------------: |
-| [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/) | Concepts | 2017/07/13 |
-| [Dockerizing GitLab Review Apps](https://about.gitlab.com/2017/07/11/dockerizing-review-apps/) | Concepts | 2017/07/11 |
-| [Continuous Integration: From Jenkins to GitLab Using Docker](https://about.gitlab.com/2017/07/27/docker-my-precious/) | Concepts | 2017/07/27 |
-| [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/) | Tutorial | 2016/12/14 |
-| [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/) | Tutorial | 2016/11/30 |
-| [Automated Debian Package Build with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/) | Tutorial | 2016/10/12 |
-| [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/) | Tutorial | 2016/08/11 |
-| [Continuous Delivery with GitLab and Convox](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/) | Technical overview | 2016/06/09 |
-| [GitLab Container Registry](https://about.gitlab.com/2016/05/23/gitlab-container-registry/) | Technical overview | 2016/05/23 |
-| [How to use GitLab CI and MacStadium to build your macOS or iOS projects](https://about.gitlab.com/2017/05/15/how-to-use-macstadium-and-gitlab-ci-to-build-your-macos-or-ios-projects/) | Technical overview | 2017/05/15 |
-| [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) | Tutorial | 2016/03/10 |
+| [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md) | Tutorial | 2017-08-31 |
+| [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md) | Tutorial | 2017-08-15 |
+| [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/) | Concepts | 2017-07-13 |
+| [Dockerizing GitLab Review Apps](https://about.gitlab.com/2017/07/11/dockerizing-review-apps/) | Concepts | 2017-07-11 |
+| [Continuous Integration: From Jenkins to GitLab Using Docker](https://about.gitlab.com/2017/07/27/docker-my-precious/) | Concepts | 2017-07-27 |
+| [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/) | Tutorial | 2016-12-14 |
+| [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/) | Tutorial | 2016-11-30 |
+| [Automated Debian Package Build with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/) | Tutorial | 2016-10-12 |
+| [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/) | Tutorial | 2016-08-11 |
+| [Continuous Delivery with GitLab and Convox](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/) | Technical overview | 2016-06-09 |
+| [GitLab Container Registry](https://about.gitlab.com/2016/05/23/gitlab-container-registry/) | Technical overview | 2016-05-23 |
+| [How to use GitLab CI and MacStadium to build your macOS or iOS projects](https://about.gitlab.com/2017/05/15/how-to-use-macstadium-and-gitlab-ci-to-build-your-macos-or-ios-projects/) | Technical overview | 2017-05-15 |
+| [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) | Tutorial | 2016-03-10 |
## Git
@@ -44,10 +46,11 @@ Learn how to use [Git with GitLab](../topics/git/index.md):
| Article title | Category | Publishing date |
| :------------ | :------: | --------------: |
-| [Why Git is Worth the Learning Curve](https://about.gitlab.com/2017/05/17/learning-curve-is-the-biggest-challenge-developers-face-with-git/) | Concepts | 2017/05/17 |
-| [How to install Git](how_to_install_git/index.md) | Tutorial | 2017/05/15 |
-| [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/) | Tutorial | 2017/01/30 |
-| [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/) | Technical overview | 2016/12/08 |
+| [Numerous _undo_ possibilities in Git](numerous_undo_possibilities_in_git/index.md) | Tutorial | 2017-08-17 |
+| [Why Git is Worth the Learning Curve](https://about.gitlab.com/2017/05/17/learning-curve-is-the-biggest-challenge-developers-face-with-git/) | Concepts | 2017-05-17 |
+| [How to install Git](how_to_install_git/index.md) | Tutorial | 2017-05-15 |
+| [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/) | Tutorial | 2017-01-30 |
+| [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/) | Technical overview | 2016-12-08 |
## GitLab Pages
@@ -56,32 +59,33 @@ Learn how to deploy a static website with [GitLab Pages](../user/project/pages/i
| Article title | Category | Publishing date |
| :------------ | :------: | --------------: |
| **Series: GitLab Pages from A to Z:** |
-| [- Part 1: Static sites and GitLab Pages domains](../user/project/pages/getting_started_part_one.md)| User guide | 2017/02/22 |
-| [- Part 2: Quick start guide - Setting up GitLab Pages](../user/project/pages/getting_started_part_two.md)| User guide | 2017/02/22 |
-| [- Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](../user/project/pages/getting_started_part_three.md)| User guide | 2017/02/22 |
-| [- Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](../user/project/pages/getting_started_part_four.md)| User guide | 2017/02/22 |
-| [Setting up GitLab Pages with CloudFlare Certificates](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) | Tutorial | 2017/02/07 |
-| [Building a new GitLab Docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) | Tutorial | 2016/12/07 |
-| [Publish Code Coverage Report with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) | Tutorial | 2016/11/03 |
-| [GitLab CI: Deployment & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) | Tutorial | 2016/08/26 |
-| [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) | Tutorial | 2016/08/19 |
+| [- Part 1: Static sites and GitLab Pages domains](../user/project/pages/getting_started_part_one.md)| User guide | 2017-02-22 |
+| [- Part 2: Quick start guide - Setting up GitLab Pages](../user/project/pages/getting_started_part_two.md)| User guide | 2017-02-22 |
+| [- Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](../user/project/pages/getting_started_part_three.md)| User guide | 2017-02-22 |
+| [- Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](../user/project/pages/getting_started_part_four.md)| User guide | 2017-02-22 |
+| [Setting up GitLab Pages with CloudFlare Certificates](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) | Tutorial | 2017-02-07 |
+| [Building a new GitLab Docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) | Tutorial | 2016-12-07 |
+| [Publish Code Coverage Report with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) | Tutorial | 2016-11-03 |
+| [GitLab CI: Deployment & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) | Tutorial | 2016-08-26 |
+| [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) | Tutorial | 2016-08-19 |
| **Series: Static Site Generator:** |
-| [- Part 1: Dynamic vs Static Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) | Tutorial | 2016/06/03 |
-| [- Part 2: Modern Static Site Generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) | Tutorial | 2016/06/10 |
-| [- Part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) | Tutorial | 2016/06/17 |
-| [Securing your GitLab Pages with TLS and Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) | Tutorial | 2016/04/11 |
-| [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) | Tutorial | 2016/04/07 |
+| [- Part 1: Dynamic vs Static Websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) | Tutorial | 2016-06-03 |
+| [- Part 2: Modern Static Site Generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) | Tutorial | 2016-06-10 |
+| [- Part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) | Tutorial | 2016-06-17 |
+| [Securing your GitLab Pages with TLS and Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) | Tutorial | 2016-04-11 |
+| [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) | Tutorial | 2016-04-07 |
## Install and maintain GitLab
-Install, upgrade, integrate, migrate to GitLab:
+[Admin](../README.md#administrator-documentation), [install](../install/README.md),
+upgrade, integrate, migrate to GitLab:
| Article title | Category | Publishing date |
| :------------ | :------: | --------------: |
-| [Video Tutorial: Idea to Production on Google Container Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/) | Tutorial | 2017/01/23 |
-| [How to Setup a GitLab Instance on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/) | Tutorial | 2016/07/13 |
-| [Get started with OpenShift Origin 3 and GitLab](openshift_and_gitlab/index.md) | Tutorial | 2016/06/28 |
-| [Getting started with GitLab and DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/) | Tutorial | 2016/04/27 |
+| [Video Tutorial: Idea to Production on Google Container Engine (GKE)](https://about.gitlab.com/2017/01/23/video-tutorial-idea-to-production-on-google-container-engine-gke/) | Tutorial | 2017-01-23 |
+| [How to Setup a GitLab Instance on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/) | Tutorial | 2016-07-13 |
+| [Get started with OpenShift Origin 3 and GitLab](openshift_and_gitlab/index.md) | Tutorial | 2016-06-28 |
+| [Getting started with GitLab and DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/) | Tutorial | 2016-04-27 |
## Software development
@@ -89,25 +93,25 @@ Explore the best of GitLab's software development's capabilities:
| Article title | Category | Publishing date |
| :------------ | :------: | --------------: |
-| [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/) | Concepts | 2017/07/13 |
-| [From 2/3 of the Self-Hosted Git Market, to the Next-Generation CI System, to Auto DevOps](https://about.gitlab.com/2017/06/29/whats-next-for-gitlab-ci/)| Concepts | 2017/06/29 |
-| [Fast and Natural Continuous Integration with GitLab CI](https://about.gitlab.com/2017/05/22/fast-and-natural-continuous-integration-with-gitlab-ci/) | Concepts | 2017/05/22 |
-| [Demo: Auto-Deploy from GitLab to an OpenShift Container Cluster](https://about.gitlab.com/2017/05/16/devops-containers-gitlab-openshift/) | Technical overview | 2017/05/16 |
-| [Demo: GitLab Service Desk](https://about.gitlab.com/2017/05/09/demo-service-desk/) | Feature highlight | 2017/05/09 |
-| [Demo: Mapping Work Versus Time, With Burndown Charts](https://about.gitlab.com/2017/04/25/mapping-work-to-do-versus-time-with-burndown-charts/) | Feature highlight | 2017/04/25 |
-| [Demo: Cloud Native Development with GitLab](https://about.gitlab.com/2017/04/18/cloud-native-demo/) | Feature highlight | 2017/04/18 |
-| [Demo: Mastering Code Review With GitLab](https://about.gitlab.com/2017/03/17/demo-mastering-code-review-with-gitlab/) | Feature highlight | 2017/03/17 |
-| [In 13 minutes from Kubernetes to a complete application development tool](https://about.gitlab.com/2016/11/14/idea-to-production/) | Technical overview | 2016/11/14 |
-| [GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) | Technical overview | 2016/10/25 |
-| [Trends in Version Control Land: Microservices](https://about.gitlab.com/2016/08/16/trends-in-version-control-land-microservices/) | Concepts | 2016/08/16 |
-| [Continuous Integration, Delivery, and Deployment with GitLab](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) | Concepts | 2016/08/05 |
-| [Trends in Version Control Land: Innersourcing](https://about.gitlab.com/2016/07/07/trends-version-control-innersourcing/) | Concepts | 2016/07/07 |
-| [Tutorial: It's all connected in GitLab](https://about.gitlab.com/2016/03/08/gitlab-tutorial-its-all-connected/) | Technical overview | 2016/03/08 |
+| [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/) | Concepts | 2017-07-13 |
+| [From 2/3 of the Self-Hosted Git Market, to the Next-Generation CI System, to Auto DevOps](https://about.gitlab.com/2017/06/29/whats-next-for-gitlab-ci/)| Concepts | 2017-06-29 |
+| [Fast and Natural Continuous Integration with GitLab CI](https://about.gitlab.com/2017/05/22/fast-and-natural-continuous-integration-with-gitlab-ci/) | Concepts | 2017-05-22 |
+| [Demo: Auto-Deploy from GitLab to an OpenShift Container Cluster](https://about.gitlab.com/2017/05/16/devops-containers-gitlab-openshift/) | Technical overview | 2017-05-16 |
+| [Demo: GitLab Service Desk](https://about.gitlab.com/2017/05/09/demo-service-desk/) | Feature highlight | 2017-05-09 |
+| [Demo: Mapping Work Versus Time, With Burndown Charts](https://about.gitlab.com/2017/04/25/mapping-work-to-do-versus-time-with-burndown-charts/) | Feature highlight | 2017-04-25 |
+| [Demo: Cloud Native Development with GitLab](https://about.gitlab.com/2017/04/18/cloud-native-demo/) | Feature highlight | 2017-04-18 |
+| [Demo: Mastering Code Review With GitLab](https://about.gitlab.com/2017/03/17/demo-mastering-code-review-with-gitlab/) | Feature highlight | 2017-03-17 |
+| [In 13 minutes from Kubernetes to a complete application development tool](https://about.gitlab.com/2016/11/14/idea-to-production/) | Technical overview | 2016-11-14 |
+| [GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) | Technical overview | 2016-10-25 |
+| [Trends in Version Control Land: Microservices](https://about.gitlab.com/2016/08/16/trends-in-version-control-land-microservices/) | Concepts | 2016-08-16 |
+| [Continuous Integration, Delivery, and Deployment with GitLab](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/) | Concepts | 2016-08-05 |
+| [Trends in Version Control Land: Innersourcing](https://about.gitlab.com/2016/07/07/trends-version-control-innersourcing/) | Concepts | 2016-07-07 |
+| [Tutorial: It's all connected in GitLab](https://about.gitlab.com/2016/03/08/gitlab-tutorial-its-all-connected/) | Technical overview | 2016-03-08 |
## Technologies
| Article title | Category | Publishing date |
| :------------ | :------: | --------------: |
-| [Why we are not leaving the cloud](https://about.gitlab.com/2017/03/02/why-we-are-not-leaving-the-cloud/) | Concepts | 2017/03/02 |
-| [Why We Chose Vue.js](https://about.gitlab.com/2016/10/20/why-we-chose-vue/) | Concepts | 2016/10/20 |
-| [Markdown Kramdown Tips & Tricks](https://about.gitlab.com/2016/07/19/markdown-kramdown-tips-and-tricks/) | Technical overview | 2016/07/19 |
+| [Why we are not leaving the cloud](https://about.gitlab.com/2017/03/02/why-we-are-not-leaving-the-cloud/) | Concepts | 2017-03-02 |
+| [Why We Chose Vue.js](https://about.gitlab.com/2016/10/20/why-we-chose-vue/) | Concepts | 2016-10-20 |
+| [Markdown Kramdown Tips & Tricks](https://about.gitlab.com/2016/07/19/markdown-kramdown-tips-and-tricks/) | Technical overview | 2016-07-19 |
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_checkbox.png b/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_checkbox.png
new file mode 100644
index 00000000000..a56c07a0da7
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_checkbox.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_page_empty_image.png b/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_page_empty_image.png
new file mode 100644
index 00000000000..b1406fed6b8
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_page_empty_image.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_page_with_image.jpg b/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_page_with_image.jpg
new file mode 100644
index 00000000000..d1f0cbc08ab
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/container_registry_page_with_image.jpg
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/deploy_keys_page.png b/doc/articles/laravel_with_gitlab_and_envoy/img/deploy_keys_page.png
new file mode 100644
index 00000000000..9aae11b8679
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/deploy_keys_page.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/environment_page.png b/doc/articles/laravel_with_gitlab_and_envoy/img/environment_page.png
new file mode 100644
index 00000000000..a06b6d417cd
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/environment_page.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/environments_page.png b/doc/articles/laravel_with_gitlab_and_envoy/img/environments_page.png
new file mode 100644
index 00000000000..d357ecda7d2
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/environments_page.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/laravel_welcome_page.png b/doc/articles/laravel_with_gitlab_and_envoy/img/laravel_welcome_page.png
new file mode 100644
index 00000000000..3bb21fd12b4
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/laravel_welcome_page.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png b/doc/articles/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png
new file mode 100644
index 00000000000..bc188f83fb1
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/laravel_with_gitlab_and_envoy.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/pipeline_page.png b/doc/articles/laravel_with_gitlab_and_envoy/img/pipeline_page.png
new file mode 100644
index 00000000000..baf8dec499c
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/pipeline_page.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/pipelines_page.png b/doc/articles/laravel_with_gitlab_and_envoy/img/pipelines_page.png
new file mode 100644
index 00000000000..d96c43bcf16
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/pipelines_page.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/pipelines_page_deploy_button.png b/doc/articles/laravel_with_gitlab_and_envoy/img/pipelines_page_deploy_button.png
new file mode 100644
index 00000000000..997db10189f
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/pipelines_page_deploy_button.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/production_server_app_directory.png b/doc/articles/laravel_with_gitlab_and_envoy/img/production_server_app_directory.png
new file mode 100644
index 00000000000..6dbc29fc25c
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/production_server_app_directory.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/production_server_current_directory.png b/doc/articles/laravel_with_gitlab_and_envoy/img/production_server_current_directory.png
new file mode 100644
index 00000000000..8a6dcccfa38
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/production_server_current_directory.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/img/secret_variables_page.png b/doc/articles/laravel_with_gitlab_and_envoy/img/secret_variables_page.png
new file mode 100644
index 00000000000..658c0b5bcac
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/img/secret_variables_page.png
Binary files differ
diff --git a/doc/articles/laravel_with_gitlab_and_envoy/index.md b/doc/articles/laravel_with_gitlab_and_envoy/index.md
new file mode 100644
index 00000000000..e0d8fb8d081
--- /dev/null
+++ b/doc/articles/laravel_with_gitlab_and_envoy/index.md
@@ -0,0 +1,680 @@
+# Test and deploy Laravel applications with GitLab CI/CD and Envoy
+
+> **[Article Type](../../development/writing_documentation.md#types-of-technical-articles):** tutorial ||
+> **Level:** intermediary ||
+> **Author:** [Mehran Rasulian](https://gitlab.com/mehranrasulian) ||
+> **Publication date:** 2017-08-31
+
+## Introduction
+
+GitLab features our applications with Continuous Integration, and it is possible to easily deploy the new code changes to the production server whenever we want.
+
+In this tutorial, we'll show you how to initialize a [Laravel](http://laravel.com/) application and setup our [Envoy](https://laravel.com/docs/envoy) tasks, then we'll jump into see how to test and deploy it with [GitLab CI/CD](../../ci/README.md) via [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/).
+
+We assume you have a basic experience with Laravel, Linux servers,
+and you know how to use GitLab.
+
+Laravel is a high quality web framework written in PHP.
+It has a great community with a [fantastic documentation](https://laravel.com/docs).
+Aside from the usual routing, controllers, requests, responses, views, and (blade) templates, out of the box Laravel provides plenty of additional services such as cache, events, localization, authentication and many others.
+
+We will use [Envoy](https://laravel.com/docs/master/envoy) as an SSH task runner based on PHP.
+It uses a clean, minimal [Blade syntax](https://laravel.com/docs/blade) to setup tasks that can run on remote servers, such as, cloning your project from the repository, installing the Composer dependencies, and running [Artisan commands](https://laravel.com/docs/artisan).
+
+## Initialize our Laravel app on GitLab
+
+We assume [you have installed a new laravel project](https://laravel.com/docs/installation#installation), so let's start with a unit test, and initialize Git for the project.
+
+### Unit Test
+
+Every new installation of Laravel (currently 5.4) comes with two type of tests, 'Feature' and 'Unit', placed in the tests directory.
+Here's a unit test from `test/Unit/ExampleTest.php`:
+
+```php
+<?php
+
+namespace Tests\Unit;
+
+...
+
+class ExampleTest extends TestCase
+{
+ public function testBasicTest()
+ {
+ $this->assertTrue(true);
+ }
+}
+```
+
+This test is as simple as asserting that the given value is true.
+
+Laravel uses `PHPUnit` for tests by default.
+If we run `vendor/bin/phpunit` we should see the green output:
+
+```bash
+vendor/bin/phpunit
+OK (1 test, 1 assertions)
+```
+
+This test will be used later for continuously testing our app with GitLab CI/CD.
+
+### Push to GitLab
+
+Since we have our app up and running locally, it's time to push the codebase to our remote repository.
+Let's create [a new project](../../gitlab-basics/create-project.md) in GitLab named `laravel-sample`.
+After that, follow the command line instructions displayed on the project's homepage to initiate the repository on our machine and push the first commit.
+
+
+```bash
+cd laravel-sample
+git init
+git remote add origin git@gitlab.example.com:<USERNAME>/laravel-sample.git
+git add .
+git commit -m 'Initial Commit'
+git push -u origin master
+```
+
+## Configure the production server
+
+Before we begin setting up Envoy and GitLab CI/CD, let's quickly make sure the production server is ready for deployment.
+We have installed LEMP stack which stands for Linux, Nginx, MySQL and PHP on our Ubuntu 16.04.
+
+### Create a new user
+
+Let's now create a new user that will be used to deploy our website and give it
+the needed permissions using [Linux ACL](https://serversforhackers.com/video/linux-acls):
+
+```bash
+# Create user deployer
+sudo adduser deployer
+# Give the read-write-execute permissions to deployer user for directory /var/www
+sudo setfacl -R -m u:deployer:rwx /var/www
+```
+
+If you don't have ACL installed on your Ubuntu server, use this command to install it:
+
+```bash
+sudo apt install acl
+```
+
+### Add SSH key
+
+Let's suppose we want to deploy our app to the production server from a private repository on GitLab. First, we need to [generate a new SSH key pair **with no passphrase**](../../ssh/README.md) for the deployer user.
+
+After that, we need to copy the private key, which will be used to connect to our server as the deployer user with SSH, to be able to automate our deployment process:
+
+```bash
+# As the deployer user on server
+#
+# Copy the content of public key to authorized_keys
+cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
+# Copy the private key text block
+cat ~/.ssh/id_rsa
+```
+
+Now, let's add it to your GitLab project as a [secret variable](../../ci/variables/README.md#secret-variables).
+Secret variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes.
+They can be added per project by navigating to the project's **Settings** > **CI/CD**.
+
+![secret variables page](img/secret_variables_page.png)
+
+To the field **KEY**, add the name `SSH_PRIVATE_KEY`, and to the **VALUE** field, paste the private key you've copied earlier.
+We'll use this variable in the `.gitlab-ci.yml` later, to easily connect to our remote server as the deployer user without entering its password.
+
+We also need to add the public key to **Project** > **Settings** > **Repository** as [Deploy Keys](../../ssh/README.md/#deploy-keys), which gives us the ability to access our repository from the server through [SSH protocol](../../gitlab-basics/command-line-commands.md/#start-working-on-your-project).
+
+
+```bash
+# As the deployer user on the server
+#
+# Copy the public key
+cat ~/.ssh/id_rsa.pub
+```
+
+![deploy keys page](img/deploy_keys_page.png)
+
+To the field **Title**, add any name you want, and paste the public key into the **Key** field.
+
+Now, let's clone our repository on the server just to make sure the `deployer` user has access to the repository.
+
+```bash
+# As the deployer user on server
+#
+git clone git@gitlab.example.com:<USERNAME>/laravel-sample.git
+```
+
+>**Note:**
+Answer **yes** if asked `Are you sure you want to continue connecting (yes/no)?`.
+It adds GitLab.com to the known hosts.
+
+### Configuring Nginx
+
+Now, let's make sure our web server configuration points to the `current/public` rather than `public`.
+
+Open the default Nginx server block configuration file by typing:
+
+```bash
+sudo nano /etc/nginx/sites-available/default
+```
+
+The configuration should be like this.
+
+```
+server {
+ root /var/www/app/current/public;
+ server_name example.com;
+ # Rest of the configuration
+}
+```
+
+>**Note:**
+You may replace the app's name in `/var/www/app/current/public` with the folder name of your application.
+
+## Setting up Envoy
+
+So we have our Laravel app ready for production.
+The next thing is to use Envoy to perform the deploy.
+
+To use Envoy, we should first install it on our local machine [using the given instructions by Laravel](https://laravel.com/docs/envoy/#introduction).
+
+### How Envoy works
+
+The pros of Envoy is that it doesn't require Blade engine, it just uses Blade syntax to define tasks.
+To start, we create an `Envoy.blade.php` in the root of our app with a simple task to test Envoy.
+
+
+```php
+@servers(['web' => 'remote_username@remote_host'])
+
+@task('list', [on => 'web'])
+ ls -l
+@endtask
+```
+
+As you may expect, we have an array within `@servers` directive at the top of the file, which contains a key named `web` with a value of the server's address (e.g. `deployer@192.168.1.1`).
+Then within our `@task` directive we define the bash commands that should be run on the server when the task is executed.
+
+On the local machine use the `run` command to run Envoy tasks.
+
+```bash
+envoy run list
+```
+
+It should execute the `list` task we defined earlier, which connects to the server and lists directory contents.
+
+Envoy is not a dependency of Laravel, therefore you can use it for any PHP application.
+
+### Zero downtime deployment
+
+Every time we deploy to the production server, Envoy downloads the latest release of our app from GitLab repository and replace it with preview's release.
+Envoy does this without any [downtime](https://en.wikipedia.org/wiki/Downtime),
+so we don't have to worry during the deployment while someone might be reviewing the site.
+Our deployment plan is to clone the latest release from GitLab repository, install the Composer dependencies and finally, activate the new release.
+
+#### @setup directive
+
+The first step of our deployment process is to define a set of variables within [@setup](https://laravel.com/docs/envoy/#setup) directive.
+You may change the `app` to your application's name:
+
+
+```php
+...
+
+@setup
+ $repository = 'git@gitlab.example.com:<USERNAME>/laravel-sample.git';
+ $releases_dir = '/var/www/app/releases';
+ $app_dir = '/var/www/app';
+ $release = date('YmdHis');
+ $new_release_dir = $releases_dir .'/'. $release;
+@endsetup
+
+...
+```
+
+- `$repository` is the address of our repository
+- `$releases_dir` directory is where we deploy the app
+- `$app_dir` is the actual location of the app that is live on the server
+- `$release` contains a date, so every time that we deploy a new release of our app, we get a new folder with the current date as name
+- `$new_release_dir` is the full path of the new release which is used just to make the tasks cleaner
+
+#### @story directive
+
+The [@story](https://laravel.com/docs/envoy/#stories) directive allows us define a list of tasks that can be run as a single task.
+Here we have three tasks called `clone_repository`, `run_composer`, `update_symlinks`. These variables are usable to making our task's codes more cleaner:
+
+
+```php
+...
+
+@story('deploy')
+ clone_repository
+ run_composer
+ update_symlinks
+@endstory
+
+...
+```
+
+Let's create these three tasks one by one.
+
+#### Clone the repository
+
+The first task will create the `releases` directory (if it doesn't exist), and then clone the `master` branch of the repository (by default) into the new release directory, given by the `$new_release_dir` variable.
+The `releases` directory will hold all our deployments:
+
+```php
+...
+
+@task('clone_repository')
+ echo 'Cloning repository'
+ [ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }}
+ git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
+@endtask
+
+...
+```
+
+While our project grows, its Git history will be very very long over time.
+Since we are creating a directory per release, it might not be necessary to have the history of the project downloaded for each release.
+The `--depth 1` option is a great solution which saves systems time and disk space as well.
+
+#### Installing dependencies with Composer
+
+As you may know, this task just navigates to the new release directory and runs Composer to install the application dependencies:
+
+```php
+...
+
+@task('run_composer')
+ echo "Starting deployment ({{ $release }})"
+ cd {{ $new_release_dir }}
+ composer install --prefer-dist --no-scripts -q -o
+@endtask
+
+...
+```
+
+#### Activate new release
+
+Next thing to do after preparing the requirements of our new release, is to remove the storage directory from it and to create two symbolic links to point the application's `storage` directory and `.env` file to the new release.
+Then, we need to create another symbolic link to the new release with the name of `current` placed in the app directory.
+The `current` symbolic link always points to the latest release of our app:
+
+```php
+...
+
+@task('update_symlinks')
+ echo "Linking storage directory"
+ rm -rf {{ $new_release_dir }}/storage
+ ln -nfs {{ $app_dir }}/storage {{ $new_release_dir }}/storage
+
+ echo 'Linking .env file'
+ ln -nfs {{ $app_dir }}/.env {{ $new_release_dir }}/.env
+
+ echo 'Linking current release'
+ ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current
+@endtask
+```
+
+As you see, we use `-nfs` as an option for `ln` command, which says that the `storage`, `.env` and `current` no longer points to the preview's release and will point them to the new release by force (`f` from `-nfs` means force), which is the case when we are doing multiple deployments.
+
+### Full script
+
+The script is ready, but make sure to change the `deployer@192.168.1.1` to your server and also change `/var/www/app` with the directory you want to deploy your app.
+
+At the end, our `Envoy.blade.php` file will look like this:
+
+```php
+@servers(['web' => 'deployer@192.168.1.1'])
+
+@setup
+ $repository = 'git@gitlab.example.com:<USERNAME>/laravel-sample.git';
+ $releases_dir = '/var/www/app/releases';
+ $app_dir = '/var/www/app';
+ $release = date('YmdHis');
+ $new_release_dir = $releases_dir .'/'. $release;
+@endsetup
+
+@story('deploy')
+ clone_repository
+ run_composer
+ update_symlinks
+@endstory
+
+@task('clone_repository')
+ echo 'Cloning repository'
+ [ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }}
+ git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
+@endtask
+
+@task('run_composer')
+ echo "Starting deployment ({{ $release }})"
+ cd {{ $new_release_dir }}
+ composer install --prefer-dist --no-scripts -q -o
+@endtask
+
+@task('update_symlinks')
+ echo "Linking storage directory"
+ rm -rf {{ $new_release_dir }}/storage
+ ln -nfs {{ $app_dir }}/storage {{ $new_release_dir }}/storage
+
+ echo 'Linking .env file'
+ ln -nfs {{ $app_dir }}/.env {{ $new_release_dir }}/.env
+
+ echo 'Linking current release'
+ ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current
+@endtask
+```
+
+One more thing we should do before any deployment is to manually copy our application `storage` folder to the `/var/www/app` directory on the server for the first time.
+You might want to create another Envoy task to do that for you.
+We also create the `.env` file in the same path to setup our production environment variables for Laravel.
+These are persistent data and will be shared to every new release.
+
+Now, we would need to deploy our app by running `envoy run deploy`, but it won't be necessary since GitLab can handle that for us with CI's [environments](../../ci/environments.md), which will be described [later](#setting-up-gitlab-ci-cd) in this tutorial.
+
+Now it's time to commit [Envoy.blade.php](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Envoy.blade.php) and push it to the `master` branch.
+To keep things simple, we commit directly to `master`, without using [feature-branches](../../workflow/gitlab_flow.md/#github-flow-as-a-simpler-alternative) since collaboration is beyond the scope of this tutorial.
+In a real world project, teams may use [Issue Tracker](../../user/project/issues/index.md) and [Merge Requests](../../user/project/merge_requests/index.md) to move their code across branches:
+
+```bash
+git add Envoy.blade.php
+git commit -m 'Add Envoy'
+git push origin master
+```
+
+## Continuous Integration with GitLab
+
+We have our app ready on GitLab, and we also can deploy it manually.
+But let's take a step forward to do it automatically with [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) method.
+We need to check every commit with a set of automated tests to become aware of issues at the earliest, and then, we can deploy to the target environment if we are happy with the result of the tests.
+
+[GitLab CI/CD](../../ci/README.md) allows us to use [Docker](https://docker.com/) engine to handle the process of testing and deploying our app.
+In the case you're not familiar with Docker, refer to [How to Automate Docker Deployments](http://paislee.io/how-to-automate-docker-deployments/).
+
+To be able to build, test, and deploy our app with GitLab CI/CD, we need to prepare our work environment.
+To do that, we'll use a Docker image which has the minimum requirements that a Laravel app needs to run.
+[There are other ways](../../ci/examples/php.md/#test-php-projects-using-the-docker-executor) to do that as well, but they may lead our builds run slowly, which is not what we want when there are faster options to use.
+
+With Docker images our builds run incredibly faster!
+
+### Create a Container Image
+
+Let's create a [Dockerfile](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Dockerfile) in the root directory of our app with the following content:
+
+```bash
+# Set the base image for subsequent instructions
+FROM php:7.1
+
+# Update packages
+RUN apt-get update
+
+# Install PHP and composer dependencies
+RUN apt-get install -qq git curl libmcrypt-dev libjpeg-dev libpng-dev libfreetype6-dev libbz2-dev
+
+# Clear out the local repository of retrieved package files
+RUN apt-get clean
+
+# Install needed extensions
+# Here you can install any other extension that you need during the test and deployment process
+RUN docker-php-ext-install mcrypt pdo_mysql zip
+
+# Install Composer
+RUN curl --silent --show-error https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
+
+# Install Laravel Envoy
+RUN composer global require "laravel/envoy=~1.0"
+```
+
+We added the [official PHP 7.1 Docker image](https://hub.docker.com/r/_/php/), which consist of a minimum installation of Debian Jessie with PHP pre-installed, and works perfectly for our use case.
+
+We used `docker-php-ext-install` (provided by the official PHP Docker image) to install the PHP extensions we need.
+
+#### Setting Up GitLab Container Registry
+
+Now that we have our `Dockerfile` let's build and push it to our [GitLab Container Registry](../../user/project/container_registry.md).
+
+> The registry is the place to store and tag images for later use. Developers may want to maintain their own registry for private, company images, or for throw-away images used only in testing. Using GitLab Container Registry means you don't need to set up and administer yet another service or use a public registry.
+
+On your GitLab project repository navigate to the **Registry** tab.
+
+![container registry page empty image](img/container_registry_page_empty_image.png)
+
+You may need to [enable Container Registry](../../user/project/container_registry.md#enable-the-container-registry-for-your-project) to your project to see this tab. You'll find it under your project's **Settings > General > Sharing and permissions**.
+
+![container registry checkbox](img/container_registry_checkbox.png)
+
+To start using Container Registry on our machine, we first need to login to the GitLab registry using our GitLab username and password:
+
+```bash
+docker login registry.gitlab.com
+```
+Then we can build and push our image to GitLab:
+
+```bash
+docker build -t registry.gitlab.com/<USERNAME>/laravel-sample .
+
+docker push registry.gitlab.com/<USERNAME>/laravel-sample
+```
+
+>**Note:**
+To run the above commands, we first need to have [Docker](https://docs.docker.com/engine/installation/) installed on our machine.
+
+Congratulations! You just pushed the first Docker image to the GitLab Registry, and if you refresh the page you should be able to see it:
+
+![container registry page with image](img/container_registry_page_with_image.jpg)
+
+>**Note:**
+You can also [use GitLab CI/CD](https://about.gitlab.com/2016/05/23/gitlab-container-registry/#use-with-gitlab-ci) to build and push your Docker images, rather than doing that on your machine.
+
+We'll use this image further down in the `.gitlab-ci.yml` configuration file to handle the process of testing and deploying our app.
+
+Let's commit the `Dockerfile` file.
+
+```bash
+git add Dockerfile
+git commit -m 'Add Dockerfile'
+git push origin master
+```
+
+### Setting up GitLab CI/CD
+
+In order to build and test our app with GitLab CI/CD, we need a file called `.gitlab-ci.yml` in our repository's root. It is similar to Circle CI and Travis CI, but built-in GitLab.
+
+Our `.gitlab-ci.yml` file will look like this:
+
+```yaml
+image: registry.gitlab.com/<USERNAME>/laravel-sample:latest
+
+services:
+ - mysql:5.7
+
+variables:
+ MYSQL_DATABASE: homestead
+ MYSQL_ROOT_PASSWORD: secret
+ DB_HOST: mysql
+ DB_USERNAME: root
+
+stages:
+ - test
+ - deploy
+
+unit_test:
+ stage: test
+ script:
+ - composer install
+ - cp .env.example .env
+ - php artisan key:generate
+ - php artisan migrate
+ - vendor/bin/phpunit
+
+deploy_production:
+ stage: deploy
+ script:
+ - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
+ - eval $(ssh-agent -s)
+ - ssh-add <(echo "$SSH_PRIVATE_KEY")
+ - mkdir -p ~/.ssh
+ - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
+
+ - ~/.composer/vendor/bin/envoy run deploy
+ environment:
+ name: production
+ url: http://192.168.1.1
+ when: manual
+ only:
+ - master
+```
+
+That's a lot to take in, isn't it? Let's run through it step by step.
+
+#### Image and Services
+
+[GitLab Runners](../../ci/runners/README.md) run the script defined by `.gitlab-ci.yml`.
+The `image` keyword tells the Runners which image to use.
+The `services` keyword defines additional images [that are linked to the main image](../../ci/docker/using_docker_images.md/#what-is-a-service).
+Here we use the container image we created before as our main image and also use MySQL 5.7 as a service.
+
+```yaml
+image: registry.gitlab.com/<USERNAME>/laravel-sample:latest
+
+services:
+ - mysql:5.7
+
+...
+```
+
+>**Note:**
+If you wish to test your app with different PHP versions and [database management systems](../../ci/services/README.md), you can define different `image` and `services` keywords for each test job.
+
+#### Variables
+
+GitLab CI/CD allows us to use [environment variables](../../ci/yaml/README.md#variables) in our jobs.
+We defined MySQL as our database management system, which comes with a superuser root created by default.
+
+So we should adjust the configuration of MySQL instance by defining `MYSQL_DATABASE` variable as our database name and `MYSQL_ROOT_PASSWORD` variable as the password of `root`.
+Find out more about MySQL variables at the [official MySQL Docker Image](https://hub.docker.com/r/_/mysql/).
+
+Also set the variables `DB_HOST` to `mysql` and `DB_USERNAME` to `root`, which are Laravel specific variables.
+We define `DB_HOST` as `mysql` instead of `127.0.0.1`, as we use MySQL Docker image as a service which [is linked to the main Docker image](../../ci/docker/using_docker_images.md/#how-services-are-linked-to-the-build).
+
+```yaml
+...
+
+variables:
+ MYSQL_DATABASE: homestead
+ MYSQL_ROOT_PASSWORD: secret
+ DB_HOST: mysql
+ DB_USERNAME: root
+
+...
+```
+
+#### Unit Test as the first job
+
+We defined the required shell scripts as an array of the [script](../../ci/yaml/README.md#script) variable to be executed when running `unit_test` job.
+
+These scripts are some Artisan commands to prepare the Laravel, and, at the end of the script, we'll run the tests by `PHPUnit`.
+
+```yaml
+...
+
+unit_test:
+ script:
+ # Install app dependencies
+ - composer install
+ # Setup .env
+ - cp .env.example .env
+ # Generate an environment key
+ - php artisan key:generate
+ # Run migrations
+ - php artisan migrate
+ # Run tests
+ - vendor/bin/phpunit
+
+...
+```
+
+#### Deploy to production
+
+The job `deploy_production` will deploy the app to the production server.
+To deploy our app with Envoy, we had to set up the `$SSH_PRIVATE_KEY` variable as an [SSH private key](../../ci/ssh_keys/README.md/#ssh-keys-when-using-the-docker-executor).
+If the SSH keys have added successfully, we can run Envoy.
+
+As mentioned before, GitLab supports [Continuous Delivery](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-delivery) methods as well.
+The [environment](../../ci/yaml/README.md#environment) keyword tells GitLab that this job deploys to the `production` environment.
+The `url` keyword is used to generate a link to our application on the GitLab Environments page.
+The `only` keyword tells GitLab CI that the job should be executed only when the pipeline is building the `master` branch.
+Lastly, `when: manual` is used to turn the job from running automatically to a manual action.
+
+```yaml
+...
+
+deploy_production:
+ script:
+ # Add the private SSH key to the build environment
+ - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
+ - eval $(ssh-agent -s)
+ - ssh-add <(echo "$SSH_PRIVATE_KEY")
+ - mkdir -p ~/.ssh
+ - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
+
+ # Run Envoy
+ - ~/.composer/vendor/bin/envoy run deploy
+
+ environment:
+ name: production
+ url: http://192.168.1.1
+ when: manual
+ only:
+ - master
+```
+
+You may also want to add another job for [staging environment](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments), to final test your application before deploying to production.
+
+### Turn on GitLab CI/CD
+
+We have prepared everything we need to test and deploy our app with GitLab CI/CD.
+To do that, commit and push `.gitlab-ci.yml` to the `master` branch. It will trigger a pipeline, which you can watch live under your project's **Pipelines**.
+
+![pipelines page](img/pipelines_page.png)
+
+Here we see our **Test** and **Deploy** stages.
+The **Test** stage has the `unit_test` build running.
+click on it to see the Runner's output.
+
+![pipeline page](img/pipeline_page.png)
+
+After our code passed through the pipeline successfully, we can deploy to our production server by clicking the **play** button on the right side.
+
+![pipelines page deploy button](img/pipelines_page_deploy_button.png)
+
+Once the deploy pipeline passed successfully, navigate to **Pipelines > Environments**.
+
+![environments page](img/environments_page.png)
+
+If something doesn't work as expected, you can roll back to the latest working version of your app.
+
+![environment page](img/environment_page.png)
+
+By clicking on the external link icon specified on the right side, GitLab opens the production website.
+Our deployment successfully was done and we can see the application is live.
+
+![laravel welcome page](img/laravel_welcome_page.png)
+
+In the case that you're interested to know how is the application directory structure on the production server after deployment, here are three directories named `current`, `releases` and `storage`.
+As you know, the `current` directory is a symbolic link that points to the latest release.
+The `.env` file consists of our Laravel environment variables.
+
+![production server app directory](img/production_server_app_directory.png)
+
+If you navigate to the `current` directory, you should see the application's content.
+As you see, the `.env` is pointing to the `/var/www/app/.env` file and also `storage` is pointing to the `/var/www/app/storage/` directory.
+
+![production server current directory](img/production_server_current_directory.png)
+
+## Conclusion
+
+We configured GitLab CI to perform automated tests and used the method of [Continuous Delivery](https://continuousdelivery.com/) to deploy to production a Laravel application with Envoy, directly from the codebase.
+
+Envoy also was a great match to help us deploy the application without writing our custom bash script and doing Linux magics.
diff --git a/doc/articles/numerous_undo_possibilities_in_git/img/branching.png b/doc/articles/numerous_undo_possibilities_in_git/img/branching.png
new file mode 100644
index 00000000000..9a80c211c99
--- /dev/null
+++ b/doc/articles/numerous_undo_possibilities_in_git/img/branching.png
Binary files differ
diff --git a/doc/articles/numerous_undo_possibilities_in_git/img/rebase_reset.png b/doc/articles/numerous_undo_possibilities_in_git/img/rebase_reset.png
new file mode 100644
index 00000000000..ac7ea9ecddc
--- /dev/null
+++ b/doc/articles/numerous_undo_possibilities_in_git/img/rebase_reset.png
Binary files differ
diff --git a/doc/articles/numerous_undo_possibilities_in_git/img/revert.png b/doc/articles/numerous_undo_possibilities_in_git/img/revert.png
new file mode 100644
index 00000000000..13b3a35ca45
--- /dev/null
+++ b/doc/articles/numerous_undo_possibilities_in_git/img/revert.png
Binary files differ
diff --git a/doc/articles/numerous_undo_possibilities_in_git/index.md b/doc/articles/numerous_undo_possibilities_in_git/index.md
new file mode 100644
index 00000000000..895bbccec08
--- /dev/null
+++ b/doc/articles/numerous_undo_possibilities_in_git/index.md
@@ -0,0 +1,497 @@
+# Numerous undo possibilities in Git
+
+> **Article [Type](../../development/writing_documentation.md#types-of-technical-articles):** tutorial ||
+> **Level:** intermediary ||
+> **Author:** [Crt Mori](https://gitlab.com/Letme) ||
+> **Publication date:** 2017-08-17
+
+## Introduction
+
+In this tutorial, we will show you different ways of undoing your work in Git, for which
+we will assume you have a basic working knowledge of. Check GitLab's
+[Git documentation](../../topics/git/index.md#git-documentation) for reference.
+Also, we will only provide some general info of the commands, which is enough
+to get you started for the easy cases/examples, but for anything more advanced please refer to the [Git book](https://git-scm.com/book/en/v2).
+
+We will explain a few different techniques to undo your changes based on the stage
+of the change in your current development. Also, keep in mind that [nothing in
+Git is really deleted.][git-autoclean-ref]
+This means that until Git automatically cleans detached commits (which cannot be
+accessed by branch or tag) it will be possible to view them with `git reflog` command
+and access them with direct commit-id. Read more about _[redoing the undo](#redoing-the-undo)_ on the section below.
+
+This guide is organized depending on the [stage of development][git-basics]
+where you want to undo your changes from and if they were shared with other developers
+or not. Because Git is tracking changes a created or edited file is in the unstaged state
+(if created it is untracked by Git). After you add it to a repository (`git add`) you put
+a file into the **staged** state, which is then committed (`git commit`) to your
+local repository. After that, file can be shared with other developers (`git push`).
+Here's what we'll cover in this tutorial:
+
+ - [Undo local changes](#undo-local-changes) which were not pushed to remote repository
+
+ - Before you commit, in both unstaged and staged state
+ - After you committed
+
+ - Undo changes after they are pushed to remote repository
+
+ - [Without history modification](#undo-remote-changes-without-changing-history) (preferred way)
+ - [With history modification](#undo-remote-changes-with-modifying-history) (requires
+ coordination with team and force pushes).
+
+ - [Usecases when modifying history is generally acceptable](#where-modifying-history-is-generally-acceptable)
+ - [How to modify history](#how-modifying-history-is-done)
+ - [How to remove sensitive information from repository](#deleting-sensitive-information-from-commits)
+
+
+### Branching strategy
+
+[Git][git-official] is a de-centralized version control system, which means that beside regular
+versioning of the whole repository, it has possibilities to exchange changes
+with other repositories. To avoid chaos with
+[multiple sources of truth][git-distributed], various
+development workflows have to be followed, and it depends on your internal
+workflow how certain changes or commits can be undone or changed.
+[GitLab Flow][gitlab-flow] provides a good
+balance between developers clashing with each other while
+developing the same feature and cooperating seamlessly, but it does not enable
+joined development of the same feature by multiple developers by default.
+When multiple developers develop the same feature on the same branch, clashing
+with every synchronization is unavoidable, but a proper or chosen Git Workflow will
+prevent that anything is lost or out of sync when feature is complete. You can also
+read through this blog post on [Git Tips & Tricks][gitlab-git-tips-n-tricks]
+to learn how to easily **do** things in Git.
+
+
+## Undo local changes
+
+Until you push your changes to any remote repository, they will only affect you.
+That broadens your options on how to handle undoing them. Still, local changes
+can be on various stages and each stage has a different approach on how to tackle them.
+
+
+### Unstaged local changes (before you commit)
+
+When a change is made, but it is not added to the staged tree, Git itself
+proposes a solution to discard changes to certain file.
+
+Suppose you edited a file to change the content using your favorite editor:
+
+```shell
+vim <file>
+```
+
+Since you did not `git add <file>` to staging, it should be under unstaged files (or
+untracked if file was created). You can confirm that with:
+
+```shell
+$ git status
+On branch master
+Your branch is up-to-date with 'origin/master'.
+Changes not staged for commit:
+ (use "git add <file>..." to update what will be committed)
+ (use "git checkout -- <file>..." to discard changes in working directory)
+
+ modified: <file>
+no changes added to commit (use "git add" and/or "git commit -a")
+```
+
+At this point there are 3 options to undo the local changes you have:
+
+ - Discard all local changes, but save them for possible re-use [later](#quickly-save-local-changes)
+
+ ```shell
+ git stash
+ ```
+
+ - Discarding local changes (permanently) to a file
+
+ ```shell
+ git checkout -- <file>
+ ```
+
+ - Discard all local changes to all files permanently
+
+ ```shell
+ git reset --hard
+ ```
+
+
+Before executing `git reset --hard`, keep in mind that there is also a way to
+just temporary store the changes without committing them using `git stash`.
+This command resets the changes to all files, but it also saves them in case
+you would like to apply them at some later time. You can read more about it in
+[section below](#quickly-save-local-changes).
+
+### Quickly save local changes
+
+You are working on a feature when a boss drops by with an urgent task. Since your
+feature is not complete, but you need to swap to another branch, you can use
+`git stash` to save what you had done, swap to another branch, commit, push,
+test, then get back to previous feature branch, do `git stash pop` and continue
+where you left.
+
+The example above shows that discarding all changes is not always a preferred option,
+but Git provides a way to save them for later, while resetting the repository to state without
+them. This is achieved by Git stashing command `git stash`, which in fact saves your
+current work and runs `git reset --hard`, but it also has various
+additional options like:
+
+ - `git stash save`, which enables including temporary commit message, which will help you identify changes, among with other options
+ - `git stash list`, which lists all previously stashed commits (yes, there can be more) that were not `pop`ed
+ - `git stash pop`, which redoes previously stashed changes and removes them from stashed list
+ - `git stash apply`, which redoes previously stashed changes, but keeps them on stashed list
+
+### Staged local changes (before you commit)
+
+Let's say you have added some files to staging, but you want to remove them from the
+current commit, yet you want to retain those changes - just move them outside
+of the staging tree. You also have an option to discard all changes with
+`git reset --hard` or think about `git stash` [as described earlier.](#quickly-save-local-changes)
+
+Lets start the example by editing a file, with your favorite editor, to change the
+content and add it to staging
+
+```
+vim <file>
+git add <file>
+```
+
+The file is now added to staging as confirmed by `git status` command:
+
+```shell
+$ git status
+On branch master
+Your branch is up-to-date with 'origin/master'.
+Changes to be committed:
+ (use "git reset HEAD <file>..." to unstage)
+
+ new file: <file>
+```
+
+Now you have 4 options to undo your changes:
+
+ - Unstage the file to current commit (HEAD)
+
+ ```shell
+ git reset HEAD <file>
+ ```
+
+ - Unstage everything - retain changes
+
+ ```shell
+ git reset
+ ```
+
+ - Discard all local changes, but save them for [later](#quickly-save-local-changes)
+
+ ```shell
+ git stash
+ ```
+
+ - Discard everything permanently
+
+ ```shell
+ git reset --hard
+ ```
+
+## Committed local changes
+
+Once you commit, your changes are recorded by the version control system.
+Because you haven't pushed to your remote repository yet, your changes are
+still not public (or shared with other developers). At this point, undoing
+things is a lot easier, we have quite some workaround options. Once you push
+your code, you'll have less options to troubleshoot your work.
+
+### Without modifying history
+
+Through the development process some of the previously committed changes do not
+fit anymore in the end solution, or are source of the bugs. Once you find the
+commit which triggered bug, or once you have a faulty commit, you can simply
+revert it with `git revert commit-id`. This command inverts (swaps) the additions and
+deletions in that commit, so that it does not modify history. Retaining history
+can be helpful in future to notice that some changes have been tried
+unsuccessfully in the past.
+
+In our example we will assume there are commits `A`,`B`,`C`,`D`,`E` committed in this order: `A-B-C-D-E`,
+and `B` is the commit you want to undo. There are many different ways to identify commit
+`B` as bad, one of them is to pass a range to `git bisect` command. The provided range includes
+last known good commit (we assume `A`) and first known bad commit (where bug was detected - we will assume `E`).
+
+```shell
+git bisect A..E
+```
+
+Bisect will provide us with commit-id of the middle commit to test, and then guide us
+through simple bisection process. You can read more about it [in official Git Tools][git-debug]
+In our example we will end up with commit `B`, that introduced bug/error. We have
+4 options on how to remove it (or part of it) from our repository.
+
+- Undo (swap additions and deletions) changes introduced by commit `B`.
+
+ ```shell
+ git revert commit-B-id
+ ```
+
+- Undo changes on a single file or directory from commit `B`, but retain them in the staged state
+
+ ```shell
+ git checkout commit-B-id <file>
+ ```
+
+- Undo changes on a single file or directory from commit `B`, but retain them in the unstaged state
+
+ ```shell
+ git reset commit-B-id <file>
+ ```
+
+ - There is one command we also must not forget: **creating a new branch**
+ from the point where changes are not applicable or where the development has hit a
+ dead end. For example you have done commits `A-B-C-D` on your feature-branch
+ and then you figure `C` and `D` are wrong. At this point you either reset to `B`
+ and do commit `F` (which will cause problems with pushing and if forced pushed also with other developers)
+ since branch now looks `A-B-F`, which clashes with what other developers have locally (you will
+ [change history](#with-history-modification)), or you simply checkout commit `B` create
+ a new branch and do commit `F`. In the last case, everyone else can still do their work while you
+ have your new way to get it right and merge it back in later. Alternatively, with GitLab,
+ you can [cherry-pick](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit)
+ that commit into a new merge request.
+
+ ![Create a new branch to avoid clashing](img/branching.png)
+
+ ```shell
+ git checkout commit-B-id
+ git checkout -b new-path-of-feature
+ # Create <commit F>
+ git commit -a
+ ```
+
+### With history modification
+
+There is one command for history modification and that is `git rebase`. Command
+provides interactive mode (`-i` flag) which enables you to:
+
+ - **reword** commit messages (there is also `git commit --amend` for editing
+ last commit message)
+ - **edit** the commit content (changes introduced by commit) and message
+ - **squash** multiple commits into a single one, and have a custom or aggregated
+ commit message
+ - **drop** commits - simply delete them
+ - and few more options
+
+Let us check few examples. Again there are commits `A-B-C-D` where you want to
+delete commit `B`.
+
+- Rebase the range from current commit D to A:
+
+ ```shell
+ git rebase -i A
+ ```
+
+- Command opens your favorite editor where you write `drop` in front of commit
+ `B`, but you leave default `pick` with all other commits. Save and exit the
+ editor to perform a rebase. Remember: if you want to cancel delete whole
+ file content before saving and exiting the editor
+
+In case you want to modify something introduced in commit `B`.
+
+- Rebase the range from current commit D to A:
+
+ ```shell
+ git rebase -i A
+ ```
+
+- Command opens your favorite text editor where you write `edit` in front of commit
+ `B`, but leave default `pick` with all other commits. Save and exit the editor to
+ perform a rebase
+
+- Now do your edits and commit changes:
+
+ ```shell
+ git commit -a
+ ```
+
+You can find some more examples in [below section where we explain how to modify
+history](#how-modifying-history-is-done)
+
+
+### Redoing the Undo
+
+Sometimes you realize that the changes you undid were useful and you want them
+back. Well because of first paragraph you are in luck. Command `git reflog`
+enables you to *recall* detached local commits by referencing or applying them
+via commit-id. Although, do not expect to see really old commits in reflog, because
+Git regularly [cleans the commits which are *unreachable* by branches or tags][git-autoclean-ref].
+
+To view repository history and to track older commits you can use below command:
+
+```shell
+$ git reflog show
+
+# Example output:
+b673187 HEAD@{4}: merge 6e43d5987921bde189640cc1e37661f7f75c9c0b: Merge made by the 'recursive' strategy.
+eb37e74 HEAD@{5}: rebase -i (finish): returning to refs/heads/master
+eb37e74 HEAD@{6}: rebase -i (pick): Commit C
+97436c6 HEAD@{7}: rebase -i (start): checkout 97436c6eec6396c63856c19b6a96372705b08b1b
+...
+88f1867 HEAD@{12}: commit: Commit D
+97436c6 HEAD@{13}: checkout: moving from 97436c6eec6396c63856c19b6a96372705b08b1b to test
+97436c6 HEAD@{14}: checkout: moving from master to 97436c6
+05cc326 HEAD@{15}: commit: Commit C
+6e43d59 HEAD@{16}: commit: Commit B
+```
+
+Output of command shows repository history. In first column there is commit-id,
+in following column, number next to `HEAD` indicates how many commits ago something
+was made, after that indicator of action that was made (commit, rebase, merge, ...)
+and then on end description of that action.
+
+## Undo remote changes without changing history
+
+This topic is roughly same as modifying committed local changes without modifying
+history. **It should be the preferred way of undoing changes on any remote repository
+or public branch.** Keep in mind that branching is the best solution when you want
+to retain the history of faulty development, yet start anew from certain point. Branching
+enables you to include the existing changes in new development (by merging) and
+it also provides a clear timeline and development structure.
+
+![Use revert to keep branch flowing](img/revert.png)
+
+If you want to revert changes introduced in certain `commit-id` you can simply
+revert that `commit-id` (swap additions and deletions) in newly created commit:
+You can do this with
+
+```shell
+git revert commit-id
+```
+
+or creating a new branch:
+
+```shell
+git checkout commit-id
+git checkout -b new-path-of-feature
+```
+
+## Undo remote changes with modifying history
+
+This is useful when you want to *hide* certain things - like secret keys,
+passwords, SSH keys, etc. It is and should not be used to hide mistakes, as
+it will make it harder to debug in case there are some other bugs. The main
+reason for this is that you loose the real development progress. **Also keep in
+mind that, even with modified history, commits are just detached and can still be
+accessed through commit-id** - at least until all repositories perform
+the cleanup of detached commits (happens automatically).
+
+![Modifying history causes problems on remote branch](img/rebase_reset.png)
+
+### Where modifying history is generally acceptable
+
+Modified history breaks the development chain of other developers, as changed
+history does not have matching commits'ids. For that reason it should not
+be used on any public branch or on branch that *might* be used by other
+developers. When contributing to big open source repositories (e.g. [GitLab CE][gitlab-ce]),
+it is acceptable to *squash* commits into a single one, to present
+a nicer history of your contribution.
+Keep in mind that this also removes the comments attached to certain commits
+in merge requests, so if you need to retain traceability in GitLab, then
+modifying history is not acceptable.
+A feature-branch of a merge request is a public branch and might be used by
+other developers, but project process and rules might allow or require
+you to use `git rebase` (command that changes history) to reduce number of
+displayed commits on target branch after reviews are done (for example
+GitLab). There is a `git merge --squash` command which does exactly that
+(squashes commits on feature-branch to a single commit on target branch
+at merge).
+
+>**Note:**
+Never modify the commit history of `master` or shared branch
+
+### How modifying history is done
+
+After you know what you want to modify (how far in history or how which range of
+old commits), use `git rebase -i commit-id`. This command will then display all the commits from
+current version to chosen commit-id and allow modification, squashing, deletion
+of that commits.
+
+```shell
+$ git rebase -i commit1-id..commit3-id
+pick <commit1-id> <commit1-commit-message>
+pick <commit2-id> <commit2-commit-message>
+pick <commit3-id> <commit3-commit-message>
+
+# Rebase commit1-id..commit3-id onto <commit4-id> (3 command(s))
+#
+# Commands:
+# p, pick = use commit
+# r, reword = use commit, but edit the commit message
+# e, edit = use commit, but stop for amending
+# s, squash = use commit, but meld into previous commit
+# f, fixup = like "squash", but discard this commit's log message
+# x, exec = run command (the rest of the line) using shell
+# d, drop = remove commit
+#
+# These lines can be re-ordered; they are executed from top to bottom.
+#
+# If you remove a line here THAT COMMIT WILL BE LOST.
+#
+# However, if you remove everything, the rebase will be aborted.
+#
+# Note that empty commits are commented out
+```
+
+>**Note:**
+It is important to notice that comment from the output clearly states that, if
+you decide to abort, then do not just close your editor (as that will in-fact
+modify history), but remove all uncommented lines and save.
+
+That is one of the reasons why `git rebase` should be used carefully on
+shared and remote branches. But don't worry, there will be nothing broken until
+you push back to the remote repository (so you can freely explore the
+different outcomes locally).
+
+```shell
+# Modify history from commit-id to HEAD (current commit)
+git rebase -i commit-id
+```
+
+### Deleting sensitive information from commits
+
+Git also enables you to delete sensitive information from your past commits and
+it does modify history in the progress. That is why we have included it in this
+section and not as a standalone topic. To do so, you should run the
+`git filter-branch`, which enables you to rewrite history with
+[certain filters][git-filters-manual].
+This command uses rebase to modify history and if you want to remove certain
+file from history altogether use:
+
+```shell
+git filter-branch --tree-filter 'rm filename' HEAD
+```
+
+Since `git filter-branch` command might be slow on big repositories, there are
+tools that can use some of Git specifics to enable faster execution of common
+tasks (which is exactly what removing sensitive information file is about).
+An alternative is [BFG Repo-cleaner][bfg-repo-cleaner]. Keep in mind that these
+tools are faster because they do not provide a same fully feature set as `git filter-branch`
+does, but focus on specific usecases.
+
+## Conclusion
+
+There are various options of undoing your work with any version control system, but
+because of de-centralized nature of Git, these options are multiplied (or limited)
+depending on the stage of your process. Git also enables rewriting history, but that
+should be avoided as it might cause problems when multiple developers are
+contributing to the same codebase.
+
+<!-- Identifiers, in alphabetical order -->
+
+[bfg-repo-cleaner]: https://rtyley.github.io/bfg-repo-cleaner/
+[git-autoclean-ref]: https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery
+[git-basics]: https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository
+[git-debug]: https://git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git
+[git-distributed]: https://git-scm.com/about/distributed
+[git-filters-manual]: https://git-scm.com/docs/git-filter-branch#_options
+[git-official]: https://git-scm.com/
+[gitlab-ce]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
+[gitlab-flow]: https://about.gitlab.com/2014/09/29/gitlab-flow/
+[gitlab-git-tips-n-tricks]: https://about.gitlab.com/2016/12/08/git-tips-and-tricks/
diff --git a/doc/articles/openshift_and_gitlab/index.md b/doc/articles/openshift_and_gitlab/index.md
index 7f76e577efa..c0bbcfe2a8a 100644
--- a/doc/articles/openshift_and_gitlab/index.md
+++ b/doc/articles/openshift_and_gitlab/index.md
@@ -3,7 +3,7 @@
> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial ||
> **Level:** intermediary ||
> **Author:** [Achilleas Pipinellis](https://gitlab.com/axil) ||
-> **Publication date:** 2016/06/28
+> **Publication date:** 2016-06-28
## Introduction
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 10ea9467942..5cfd82de381 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -44,6 +44,10 @@ digging into specific reference guides.
- [User permissions](../user/permissions.md#gitlab-ci)
- [Jobs permissions](../user/permissions.md#jobs-permissions)
+## Auto DevOps
+
+- [Auto DevOps](../topics/autodevops/index.md)
+
## GitLab CI + Docker
Leverage the power of Docker to run your CI pipelines.
@@ -111,7 +115,9 @@ Here is an collection of tutorials and guides on setting up your CI pipeline.
- [Phoenix](examples/test-phoenix-application.md)
- [Run PHP Composer & NPM scripts then deploy them to a staging server](examples/deployment/composer-npm-deploy.md)
- [Analyze code quality with the Code Climate CLI](examples/code_climate.md)
-- **Blog posts**
+- **Articles**
+ - [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](../articles/laravel_with_gitlab_and_envoy/index.md)
+ - [How to deploy Maven projects to Artifactory with GitLab CI/CD](../articles/artifactory_and_gitlab/index.md)
- [Automated Debian packaging](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
- [Spring boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
- [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md
deleted file mode 100644
index 98f37935427..00000000000
--- a/doc/ci/api/README.md
+++ /dev/null
@@ -1 +0,0 @@
-This document was moved to a [new location](../../api/ci/README.md).
diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md
deleted file mode 100644
index 0563a367609..00000000000
--- a/doc/ci/api/builds.md
+++ /dev/null
@@ -1 +0,0 @@
-This document was moved to a [new location](../../api/ci/builds.md).
diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md
deleted file mode 100644
index 1027363851c..00000000000
--- a/doc/ci/api/runners.md
+++ /dev/null
@@ -1 +0,0 @@
-This document was moved to a [new location](../../api/ci/runners.md).
diff --git a/doc/ci/autodeploy/img/auto_deploy_btn.png b/doc/ci/autodeploy/img/auto_deploy_btn.png
new file mode 100644
index 00000000000..25915ed1c9d
--- /dev/null
+++ b/doc/ci/autodeploy/img/auto_deploy_btn.png
Binary files differ
diff --git a/doc/ci/autodeploy/img/auto_deploy_dropdown.png b/doc/ci/autodeploy/img/auto_deploy_dropdown.png
index b93b0a08fea..5815937a4af 100644
--- a/doc/ci/autodeploy/img/auto_deploy_dropdown.png
+++ b/doc/ci/autodeploy/img/auto_deploy_dropdown.png
Binary files differ
diff --git a/doc/ci/autodeploy/img/auto_monitoring.png b/doc/ci/autodeploy/img/auto_monitoring.png
new file mode 100644
index 00000000000..5661b50841b
--- /dev/null
+++ b/doc/ci/autodeploy/img/auto_monitoring.png
Binary files differ
diff --git a/doc/ci/autodeploy/img/guide_connect_cluster.png b/doc/ci/autodeploy/img/guide_connect_cluster.png
new file mode 100644
index 00000000000..b856b81a1d0
--- /dev/null
+++ b/doc/ci/autodeploy/img/guide_connect_cluster.png
Binary files differ
diff --git a/doc/ci/autodeploy/img/guide_integration.png b/doc/ci/autodeploy/img/guide_integration.png
new file mode 100644
index 00000000000..723b2619ea2
--- /dev/null
+++ b/doc/ci/autodeploy/img/guide_integration.png
Binary files differ
diff --git a/doc/ci/autodeploy/img/guide_secret.png b/doc/ci/autodeploy/img/guide_secret.png
new file mode 100644
index 00000000000..01f5aa49908
--- /dev/null
+++ b/doc/ci/autodeploy/img/guide_secret.png
Binary files differ
diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md
index 9fa2b2c4969..474cb28b9e4 100644
--- a/doc/ci/autodeploy/index.md
+++ b/doc/ci/autodeploy/index.md
@@ -1,8 +1,16 @@
-# Auto deploy
+# Auto Deploy
> [Introduced][mr-8135] in GitLab 8.15.
-> Auto deploy is an experimental feature and is not recommended for Production use at this time.
-> As of GitLab 9.1, access to the container registry is only available while the Pipeline is running. Restarting a pod, scaling a service, or other actions which require on-going access will fail. On-going secure access is planned for a subsequent release.
+> Auto deploy is an experimental feature and is **not recommended for Production use** at this time.
+
+> As of GitLab 9.1, access to the container registry is only available while the
+Pipeline is running. Restarting a pod, scaling a service, or other actions which
+require on-going access **will fail**. On-going secure access is planned for a
+subsequent release.
+
+> As of GitLab 10.0, Auto Deploy templates are **deprecated** and the
+functionality has been included in [Auto
+DevOps](../../topics/autodevops/index.md).
Auto deploy is an easy way to configure GitLab CI for the deployment of your
application. GitLab Community maintains a list of `.gitlab-ci.yml`
@@ -11,9 +19,23 @@ powering them. These scripts are responsible for packaging your application,
setting up the infrastructure and spinning up necessary services (for
example a database).
-You can use [project services][project-services] to store credentials to
-your infrastructure provider and they will be available during the
-deployment.
+## How it works
+
+The Autodeploy templates are based on the [kubernetes-deploy][kube-deploy]
+project which is used to simplify the deployment process to Kubernetes by
+providing intelligent `build`, `deploy`, and `destroy` commands which you can
+use in your `.gitlab-ci.yml` as is. It uses [Herokuish](https://github.com/gliderlabs/herokuish),
+which uses [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks)
+to do some of the work, plus some of GitLab's own tools to package it all up. For
+your convenience, a [Docker image][kube-image] is also provided.
+
+You can use the [Kubernetes project service](../../user/project/integrations/kubernetes.md)
+to store credentials to your infrastructure provider and they will be available
+during the deployment.
+
+## Quick start
+
+We made a [simple guide](quick_start_guide.md) to using Auto Deploy with GitLab.com.
## Supported templates
@@ -22,20 +44,27 @@ The list of supported auto deploy templates is available in the
## Configuration
+>**Note:**
+In order to understand why the following steps are required, read the
+[how it works](#how-it-works) section.
+
+To configure Autodeploy, you will need to:
+
1. Enable a deployment [project service][project-services] to store your
-credentials. For example, if you want to deploy to OpenShift you have to
-enable [Kubernetes service][kubernetes-service].
-1. Configure GitLab Runner to use Docker or Kubernetes executor with
-[privileged mode enabled][docker-in-docker].
+ credentials. For example, if you want to deploy to OpenShift you have to
+ enable [Kubernetes service][kubernetes-service].
+1. Configure GitLab Runner to use the
+ [Docker or Kubernetes executor](https://docs.gitlab.com/runner/executors/) with
+ [privileged mode enabled][docker-in-docker].
1. Navigate to the "Project" tab and click "Set up auto deploy" button.
![Auto deploy button](img/auto_deploy_button.png)
1. Select a template.
![Dropdown with auto deploy templates](img/auto_deploy_dropdown.png)
1. Commit your changes and create a merge request.
1. Test your deployment configuration using a [Review App][review-app] that was
-created automatically for you.
+ created automatically for you.
-## Private Project Support
+## Private project support
> Experimental support [introduced][mr-2] in GitLab 9.1.
@@ -43,7 +72,7 @@ When a project has been marked as private, GitLab's [Container Registry][contain
After the pipeline completes, Kubernetes will no longer be able to access the container registry. Restarting a pod, scaling a service, or other actions which require on-going access to the registry will fail. On-going secure access is planned for a subsequent release.
-## PostgreSQL Database Support
+## PostgreSQL database support
> Experimental support [introduced][mr-8] in GitLab 9.1.
@@ -51,13 +80,38 @@ In order to support applications that require a database, [PostgreSQL][postgresq
PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRES` to `"yes"`.
-### PostgreSQL Variables
+The following PostgreSQL variables are supported:
1. `DISABLE_POSTGRES: "yes"`: disable automatic deployment of PostgreSQL
1. `POSTGRES_USER: "my-user"`: use custom username for PostgreSQL
1. `POSTGRES_PASSWORD: "password"`: use custom password for PostgreSQL
1. `POSTGRES_DB: "my database"`: use custom database name for PostgreSQL
+## Auto Monitoring
+
+> Introduced in [GitLab 9.5](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438).
+
+Apps auto-deployed using one the [Kubernetes templates](#supported-templates) can also be automatically monitored for:
+
+* Response Metrics: latency, throughput, error rate
+* System Metrics: CPU utilization, memory utilization
+
+Metrics are gathered from [nginx-ingress](../../user/project/integrations/prometheus_library/nginx_ingress.md) and [Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md).
+
+To view the metrics, open the [Monitoring dashboard for a deployed environment](../environments.md#monitoring-environments).
+
+![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 configuration is required.
+
+If you have installed GitLab using a different method:
+
+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"`.
+
[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
[mr-2]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/2
[mr-8]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/8
@@ -67,5 +121,7 @@ PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRE
[kubernetes-service]: ../../user/project/integrations/kubernetes.md
[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../review_apps/index.md
+[kube-image]: https://gitlab.com/gitlab-examples/kubernetes-deploy/container_registry "Kubernetes deploy Container Registry"
+[kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project"
[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html
[postgresql]: https://www.postgresql.org/
diff --git a/doc/ci/autodeploy/quick_start_guide.md b/doc/ci/autodeploy/quick_start_guide.md
new file mode 100644
index 00000000000..f76c2a2cf31
--- /dev/null
+++ b/doc/ci/autodeploy/quick_start_guide.md
@@ -0,0 +1,95 @@
+# Auto Deploy: quick start guide
+
+This is a step-by-step guide to deploying a project hosted on GitLab.com to Google Cloud, using Auto Deploy.
+
+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:
+
+* `server.rb` - our application. It will start an HTTP server on port 5000 and render “Hello, world!â€
+* `Dockerfile` - to build our app into a container image. It will use a ruby base image and run `server.rb`
+
+## 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.
+
+## 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.
+
+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):
+
+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`
+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`.
+
+![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.
+
+![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.
+
+![connect to cluster](img/guide_integration.png)
+
+For API URL, you should use the `Endpoint` IP from your cluster page on Google Cloud Platform.
+
+## Expose the application to the internet
+
+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.
+
+### Set up Ingress controller
+
+You’ll need to make sure you have an ingress controller. If you don’t have one, do:
+
+```sh
+brew install kubernetes-helm
+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.
+
+### Point DNS at Cluster IP
+
+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>` pointing to the external IP address you found above.
+
+Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is assigned to the cluster IP.
+
+## Setup Auto Deploy
+
+Visit the home page of your GitLab.com project and press "Set up Auto Deploy" button.
+
+![auto deploy button](img/auto_deploy_btn.png)
+
+You will be redirected to the "New file" page where you can apply one of the Auto Deploy templates. Select "Kubernetes" to apply the template, then in the file, replace `domain.example.com` with your domain name and make any other adjustments you need.
+
+![auto deploy template](img/auto_deploy_dropdown.png)
+
+Change the target branch to `master`, and submit your changes. This should create
+a new pipeline with several jobs. If you made only the domain name change, the
+pipeline will have three jobs: `build`, `staging`, and `production`.
+
+The `build` job will create a Docker image with your new change and push it to
+the GitLab Container Registry. The `staging` job will deploy this image on your
+cluster. 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 `ruby-autodeploy-23`, but with a unique ID for your project, and
+your app will be listed as "staging" under the "Deployment" tab.
+
+Once its ready - just visit http://minimal-ruby-app-staging.yourdomain.com to see “Hello, world!â€
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index f7c2a0ef0ca..f28c9791bee 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -134,7 +134,7 @@ In order to do that, follow the steps:
# When using dind, it's wise to use the overlayfs driver for
# improved performance.
variables:
- DOCKER_DRIVER: overlay
+ DOCKER_DRIVER: overlay2
services:
- docker:dind
@@ -248,7 +248,7 @@ aware of the following implications:
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 `overlay`.
+which can be avoided if a different driver is used, for example `overlay2`.
1. Make sure a recent kernel is used, preferably `>= 4.2`.
1. Check whether the `overlay` module is loaded:
@@ -275,8 +275,12 @@ which can be avoided if a different driver is used, for example `overlay`.
```
variables:
- DOCKER_DRIVER: overlay
+ DOCKER_DRIVER: overlay2
```
+
+> **Note:**
+- 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/).
## Using the GitLab Container Registry
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index d3433594eb7..fa823ea4721 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -96,7 +96,7 @@ services:
- tutum/wordpress:latest
```
-If you don't [specify a service alias](#available-settings-for-services-entry),
+If you don't [specify a service alias](#available-settings-for-services),
when the job is run, `tutum/wordpress` will be started and you will have
access to it from your build container under two hostnames to choose from:
@@ -117,7 +117,7 @@ following these rules:
created (requires GitLab Runner v1.1.0 or higher)
To override the default behavior, you can
-[specify a service alias](#available-settings-for-services-entry).
+[specify a service alias](#available-settings-for-services).
## Define `image` and `services` from `.gitlab-ci.yml`
@@ -183,8 +183,7 @@ test:
## Extended Docker configuration options
-> **Note:**
-This feature requires GitLab 9.4 and GitLab Runner 9.4 or higher.
+> Introduced in GitLab and GitLab Runner 9.4.
When configuring the `image` or `services` entries, you can use a string or a map as
options:
@@ -221,28 +220,29 @@ For example, the following two definitions are equal:
### Available settings for `image`
-> **Note:**
-This feature requires GitLab 9.4 and GitLab Runner 9.4 or higher.
+> Introduced in GitLab and GitLab Runner 9.4.
-| Setting | Required | Description |
-|------------|----------|-------------|
-| `name` | yes, when used with any other option | Full name of the image that should be used. It should contain the Registry part if needed. |
-| `entrypoint` | no | Command or script that should be executed as the container's entrypoint. It will be translated to Docker's `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`][entrypoint] directive, where each shell token is a separate string in the array. |
+| Setting | Required | GitLab version | Description |
+|------------|----------|----------------| ----------- |
+| `name` | yes, when used with any other option | 9.4 |Full name of the image that should be used. It should contain the Registry part if needed. |
+| `entrypoint` | no | 9.4 |Command or script that should be executed as the container's entrypoint. It will be translated to Docker's `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`][entrypoint] directive, where each shell token is a separate string in the array. |
### Available settings for `services`
-> **Note:**
-This feature requires GitLab 9.4 and GitLab Runner 9.4 or higher.
+> Introduced in GitLab and GitLab Runner 9.4.
-| Setting | Required | Description |
-|------------|----------|-------------|
-| `name` | yes, when used with any other option | Full name of the image that should be used. It should contain the Registry part if needed. |
-| `entrypoint` | no | Command or script that should be executed as the container's entrypoint. It will be translated to Docker's `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`][entrypoint] directive, where each shell token is a separate string in the array. |
-| `command` | no | Command or script that should be used as the container's command. It will be translated to arguments passed to Docker after the image's name. The syntax is similar to [`Dockerfile`'s `CMD`][cmd] directive, where each shell token is a separate string in the array. |
-| `alias` | no | Additional alias that can be used to access the service from the job's container. Read [Accessing the services](#accessing-the-services) for more information. |
+| Setting | Required | GitLab version | Description |
+|------------|----------|----------------| ----------- |
+| `name` | yes, when used with any other option | 9.4 | Full name of the image that should be used. It should contain the Registry part if needed. |
+| `entrypoint` | no | 9.4 |Command or script that should be executed as the container's entrypoint. It will be translated to Docker's `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`][entrypoint] directive, where each shell token is a separate string in the array. |
+| `command` | no | 9.4 |Command or script that should be used as the container's command. It will be translated to arguments passed to Docker after the image's name. The syntax is similar to [`Dockerfile`'s `CMD`][cmd] directive, where each shell token is a separate string in the array. |
+| `alias` | no | 9.4 |Additional alias that can be used to access the service from the job's container. Read [Accessing the services](#accessing-the-services) for more information. |
### Starting multiple services from the same image
+> Introduced in GitLab and GitLab Runner 9.4. Read more about the [extended
+configuration options](#extended-docker-configuration-options).
+
Before the new extended Docker configuration options, the following configuration
would not work properly:
@@ -274,6 +274,9 @@ in `.gitlab-ci.yml` file.
### Setting a command for the service
+> Introduced in GitLab and GitLab Runner 9.4. Read more about the [extended
+configuration options](#extended-docker-configuration-options).
+
Let's assume you have a `super/sql:latest` image with some SQL database
inside it and you would like to use it as a service for your job. Let's also
assume that this image doesn't start the database process while starting
@@ -313,6 +316,9 @@ As you can see, the syntax of `command` is similar to [Dockerfile's `CMD`][cmd].
### Overriding the entrypoint of an image
+> Introduced in GitLab and GitLab Runner 9.4. Read more about the [extended
+configuration options](#extended-docker-configuration-options).
+
Let's assume you have a `super/sql:experimental` image with some SQL database
inside it and you would like to use it as a base image for your job because you
want to execute some tests with this database binary. Let's also assume that
@@ -382,15 +388,40 @@ that runner.
As an example, let's assume that you want to use the `registry.example.com/private/image:latest`
image which is private and requires you to login into a private container registry.
+
+Let's also assume that these are the login credentials:
+
+| Key | Value |
+|----------|----------------------|
+| registry | registry.example.com |
+| username | my_username |
+| password | my_password |
+
To configure access for `registry.example.com`, follow these steps:
-1. Do a `docker login` on your computer:
+1. Find what the value of `DOCKER_AUTH_CONFIG` should be. There are two ways to
+ accomplish this:
+ - **First way -** Do a `docker login` on your local machine:
- ```bash
- docker login registry.example.com --username my_username --password my_password
- ```
+ ```bash
+ docker login registry.example.com --username my_username --password my_password
+ ```
+
+ Then copy the content of `~/.docker/config.json`.
+ - **Second way -** In some setups, it's possible that Docker client will use
+ the available system keystore to store the result of `docker login`. In
+ that case, it's impossible to read `~/.docker/config.json`, so you will
+ need to prepare the required base64-encoded version of
+ `${username}:${password}` manually. Open a terminal and execute the
+ following command:
+
+ ```bash
+ echo -n "my_username:my_password" | base64
+
+ # Example output to copy
+ bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=
+ ```
-1. Copy the content of `~/.docker/config.json`
1. Create a [secret variable] `DOCKER_AUTH_CONFIG` with the content of the
Docker configuration file as the value:
@@ -404,7 +435,8 @@ To configure access for `registry.example.com`, follow these steps:
}
```
-1. Do a `docker logout` on your computer if you don't need access to the
+1. Optionally,if you followed the first way of finding the `DOCKER_AUTH_CONFIG`
+ value, do a `docker logout` on your computer if you don't need access to the
registry from it:
```bash
@@ -412,7 +444,7 @@ To configure access for `registry.example.com`, follow these steps:
```
1. You can now use any private image from `registry.example.com` defined in
- `image` and/or `services` in your [`.gitlab-ci.yml` file][yaml-priv-reg]:
+ `image` and/or `services` in your `.gitlab-ci.yml` file:
```yaml
image: my.registry.tld:5000/namespace/image:tag
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 6a7f694d705..cbf06afa294 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -274,9 +274,7 @@ 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. You
-can use [Koding](../administration/integration/koding.md) for online
-development.
+changes! Treat this as a debugging tool, not a comprehensive online IDE.
---
@@ -607,10 +605,9 @@ exist, you should see something like:
- With GitLab 9.2, all deployments to an environment are shown directly on the
monitoring dashboard
-If you have enabled Prometheus for collecting metrics, you can monitor the performance behavior of your app
-through the environments.
+If you have enabled [Prometheus for monitoring system and response metrics](https://docs.gitlab.com/ee/user/project/integrations/prometheus.html), you can monitor the performance behavior of your app running in each environment.
-Once configured, GitLab will attempt to retrieve performance metrics for any
+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.
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 2458cb959ab..f094546c3bd 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -50,12 +50,15 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
- **Articles:**
- [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
+### Code quality analysis
+
+- [Analyze code quality with the Code Climate CLI](code_climate.md)
+
### Other
- [Using `dpl` as deployment tool](deployment/README.md)
- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-- [Analyze code quality with the Code Climate CLI](code_climate.md)
- **Articles:**
- [Continuous Deployment with GitLab: how to build and deploy a Debian Package with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md
index 5659a8c2a2a..4d0ba8bfef3 100644
--- a/doc/ci/examples/code_climate.md
+++ b/doc/ci/examples/code_climate.md
@@ -5,10 +5,10 @@ GitLab CI and Docker.
First, you need GitLab Runner with [docker-in-docker executor][dind].
-Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codeclimate`:
+Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codequality`:
```yaml
-codeclimate:
+codequality:
image: docker:latest
variables:
DOCKER_DRIVER: overlay
@@ -22,7 +22,7 @@ codeclimate:
paths: [codeclimate.json]
```
-This will create a `codeclimate` job in your CI pipeline and will allow you to
+This will create a `codequality` job in your CI pipeline and will allow you to
download and analyze the report artifact in JSON format.
For GitLab [Enterprise Edition Starter][ee] users, this information can be automatically
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 5a2b61fb0cb..ac4a9b0ed27 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -222,6 +222,30 @@ total running time should be:
Pipeline status and test coverage report badges are available. You can find their
respective link in the [Pipelines settings] page.
+## Security on protected branches
+
+A strict security model is enforced when pipelines are executed on
+[protected branches](../user/project/protected_branches.md).
+
+The following actions are allowed on protected branches only if the user is
+[allowed to merge or push](../user/project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings)
+on that specific branch:
+- run **manual pipelines** (using Web UI or Pipelines API)
+- run **scheduled pipelines**
+- run pipelines using **triggers**
+- trigger **manual actions** on existing pipelines
+- **retry/cancel** existing jobs (using Web UI or Pipelines API)
+
+**Secret variables** marked as **protected** are accessible only to jobs that
+run on protected branches, avoiding untrusted users to get unintended access to
+sensitive information like deployment credentials and tokens.
+
+**Runners** marked as **protected** can run jobs only on protected
+branches, avoiding untrusted code to be executed on the protected runner and
+preserving deployment keys and other credentials from being unintentionally
+accessed. In order to ensure that jobs intended to be executed on protected
+runners will not use regular runners, they must be tagged accordingly.
+
[jobs]: #jobs
[jobs-yaml]: yaml/README.md#jobs
[manual]: yaml/README.md#manual
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 76d746155eb..f5d3b524d6e 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -107,6 +107,43 @@ To lock/unlock a Runner:
1. Check the **Lock to current projects** option
1. Click **Save changes** for the changes to take effect
+## Assigning a Runner to another project
+
+If you are Master on a project where a specific Runner is assigned to, and the
+Runner is not [locked only to that project](#locking-a-specific-runner-from-being-enabled-for-other-projects),
+you can enable the Runner also on any other project where you have Master permissions.
+
+To enable/disable a Runner in your project:
+
+1. Visit your project's **Settings âž” Pipelines**
+1. Find the Runner you wish to enable/disable
+1. Click **Enable for this project** or **Disable for this project**
+
+> **Note**:
+Consider that if you don't lock your specific Runner to a specific project, any
+user with Master role in you project can assign your runner to another arbitrary
+project without requiring your authorization, so use it with caution.
+
+## Protected Runners
+
+>
+[Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13194)
+in GitLab 10.0.
+
+You can protect Runners from revealing sensitive information.
+Whenever a Runner is protected, the Runner picks only jobs created on
+[protected branches] or [protected tags], and ignores other jobs.
+
+To protect/unprotect Runners:
+
+1. Visit your project's **Settings âž” Pipelines**
+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
+1. Click **Save changes** for the changes to take effect
+
+![specific Runners edit icon](img/protected_runners_check_box.png)
+
## How shared Runners pick jobs
Shared Runners abide to a process queue we call fair usage. The fair usage
@@ -218,3 +255,5 @@ We're always looking for contributions that can mitigate these
[install]: http://docs.gitlab.com/runner/install/
[fifo]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)
[register]: http://docs.gitlab.com/runner/register/
+[protected branches]: ../../user/project/protected_branches.md
+[protected tags]: ../../user/project/protected_tags.md
diff --git a/doc/ci/runners/img/protected_runners_check_box.png b/doc/ci/runners/img/protected_runners_check_box.png
new file mode 100644
index 00000000000..fb58498c7ce
--- /dev/null
+++ b/doc/ci/runners/img/protected_runners_check_box.png
Binary files differ
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index cf25a8b618f..cdb9858e179 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -42,7 +42,7 @@ It is also good practice to check the server's own public key to make sure you
are not being targeted by a man-in-the-middle attack. To do this, add another
variable named `SSH_SERVER_HOSTKEYS`. To find out the hostkeys of your server, run
the `ssh-keyscan YOUR_SERVER` command from a trusted network (ideally, from the
-server itself), and paste its output into the `SSH_SERVER_HOSTKEY` variable. If
+server itself), and paste its output into the `SSH_SERVER_HOSTKEYS` variable. If
you need to connect to multiple servers, concatenate all the server public keys
that you collected into the **Value** of the variable. There must be one key per
line.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 22e7f6879ed..6513b31826a 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -57,6 +57,7 @@ future GitLab releases.**
| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
+| **CI_PIPELINE_SOURCE** | 10.0 | all | The source for this pipeline, one of: push, web, trigger, schedule, api, external. Pipelines created before 9.5 will have unknown as source |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) |
@@ -77,6 +78,8 @@ future GitLab releases.**
| **GITLAB_CI** | all | all | Mark that job is executed in GitLab CI environment |
| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job |
| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job |
+| **GITLAB_USER_LOGIN** | 10.0 | all | The login username of the user who started the job |
+| **GITLAB_USER_NAME** | 10.0 | all | The real name of the user who started the job |
| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
## 9.0 Renaming
@@ -85,6 +88,11 @@ To follow conventions of naming across GitLab, and to futher move away from the
`build` term and toward `job` CI variables have been renamed for the 9.0
release.
+>**Note:**
+Starting with GitLab 9.0, we have deprecated the `$CI_BUILD_*` variables. **You are
+strongly advised to use the new variables as we will remove the old ones in
+future GitLab releases.**
+
| 8.x name | 9.0+ name |
| --------------------- |------------------------ |
| `CI_BUILD_ID` | `CI_JOB_ID` |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 1869782fe6e..d0ac3ec6163 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -130,7 +130,7 @@ There are also two edge cases worth mentioning:
### types
-> Deprecated, and will be removed in 10.0. Use [stages](#stages) instead.
+> Deprecated, and could be removed in one of the future releases. Use [stages](#stages) instead.
Alias for [stages](#stages).
@@ -427,16 +427,16 @@ a "key: value" pair. Be careful when using special characters:
are executed in `parallel`. For more info about the use of `stage` please check
[stages](#stages).
-### only and except
+### only and except (simplified)
-`only` and `except` are two parameters that set a refs policy to limit when
-jobs are built:
+`only` and `except` are two parameters that set a job policy to limit when
+jobs are created:
1. `only` defines the names of branches and tags for which the job will run.
2. `except` defines the names of branches and tags for which the job will
**not** run.
-There are a few rules that apply to the usage of refs policy:
+There are a few rules that apply to the usage of job policy:
* `only` and `except` are inclusive. If both `only` and `except` are defined
in a job specification, the ref is filtered by `only` and `except`.
@@ -497,6 +497,36 @@ job:
The above example will run `job` for all branches on `gitlab-org/gitlab-ce`,
except master.
+### only and except (complex)
+
+> Introduced in GitLab 10.0
+
+> This an _alpha_ feature, and it it subject to change at any time without
+ prior notice!
+
+Since GitLab 10.0 it is possible to define a more elaborate only/except job
+policy configuration.
+
+GitLab now supports both, simple and complex strategies, so it is possible to
+use an array and a hash configuration scheme.
+
+Two keys are now available: `refs` and `kubernetes`. Refs strategy equals to
+simplified only/except configuration, whereas kubernetes strategy accepts only
+`active` keyword.
+
+See the example below. Job is going to be created only when pipeline has been
+scheduled or runs for a `master` branch, and only if kubernetes service is
+active in the project.
+
+```yaml
+job:
+ only:
+ refs:
+ - master
+ - schedules
+ kubernetes: active
+```
+
### Job variables
It is possible to define job variables using a `variables` keyword on a job
@@ -1065,6 +1095,8 @@ a list of all previous jobs from which the artifacts should be downloaded.
You can only define jobs from stages that are executed before the current one.
An error will be shown if you define jobs from the current stage or next ones.
Defining an empty array will skip downloading any artifacts for that job.
+The status of the previous job is not considered when using `dependencies`, so
+if it failed or it is a manual job that was not run, no error occurs.
---
diff --git a/doc/development/README.md b/doc/development/README.md
index 58993c52dcd..dd150421b65 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -46,6 +46,7 @@
## Databases
+- [Merge Request Checklist](database_merge_request_checklist.md)
- [What requires downtime?](what_requires_downtime.md)
- [Adding database indexes](adding_database_indexes.md)
- [Post Deployment Migrations](post_deployment_migrations.md)
@@ -56,6 +57,9 @@
- [Background Migrations](background_migrations.md)
- [Storing SHA1 Hashes As Binary](sha1_as_binary.md)
- [Iterating Tables In Batches](iterating_tables_in_batches.md)
+- [Ordering Table Columns](ordering_table_columns.md)
+- [Verifying Database Capabilities](verifying_database_capabilities.md)
+- [Hash Indexes](hash_indexes.md)
## i18n
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index ce39a379a0e..f869938fe11 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -15,11 +15,14 @@ following format:
title: "Going through change[log]s"
merge_request: 1972
author: Ozzy Osbourne
+type: added
```
The `merge_request` value is a reference to a merge request that adds this
entry, and the `author` key is used to give attribution to community
contributors. **Both are optional**.
+The `type` field maps the category of the change,
+valid options are: added, fixed, changed, deprecated, removed, security, other. **Type field is mandatory**.
Community contributors and core team members are encouraged to add their name to
the `author` field. GitLab team members **should not**.
@@ -94,6 +97,19 @@ Its simplest usage is to provide the value for `title`:
$ bin/changelog 'Hey DZ, I added a feature to GitLab!'
```
+At this point the script would ask you to select the category of the change (mapped to the `type` field in the entry):
+
+```text
+>> Please specify the category of your change:
+1. New feature
+2. Bug fix
+3. Feature change
+4. New deprecation
+5. Feature removal
+6. Security fix
+7. Other
+```
+
The entry filename is based on the name of the current Git branch. If you run
the command above on a branch called `feature/hey-dz`, it will generate a
`changelogs/unreleased/feature-hey-dz.yml` file.
@@ -106,26 +122,29 @@ create changelogs/unreleased/my-feature.yml
title: Hey DZ, I added a feature to GitLab!
merge_request:
author:
+type:
```
If you're working on the GitLab EE repository, the entry will be added to
`changelogs/unreleased-ee/` instead.
#### Arguments
-| Argument | Shorthand | Purpose |
-| ----------------- | --------- | --------------------------------------------- |
-| [`--amend`] | | Amend the previous commit |
-| [`--force`] | `-f` | Overwrite an existing entry |
-| [`--merge-request`] | `-m` | Set merge request ID |
-| [`--dry-run`] | `-n` | Don't actually write anything, just print |
-| [`--git-username`] | `-u` | Use Git user.name configuration as the author |
-| [`--help`] | `-h` | Print help message |
+| Argument | Shorthand | Purpose |
+| ----------------- | --------- | ---------------------------------------------------------------------------------------------------------- |
+| [`--amend`] | | Amend the previous commit |
+| [`--force`] | `-f` | Overwrite an existing entry |
+| [`--merge-request`] | `-m` | Set merge request ID |
+| [`--dry-run`] | `-n` | Don't actually write anything, just print |
+| [`--git-username`] | `-u` | Use Git user.name configuration as the author |
+| [`--type`] | `-t` | The category of the change, valid options are: added, fixed, changed, deprecated, removed, security, other |
+| [`--help`] | `-h` | Print help message |
[`--amend`]: #-amend
[`--force`]: #-force-or-f
[`--merge-request`]: #-merge-request-or-m
[`--dry-run`]: #-dry-run-or-n
[`--git-username`]: #-git-username-or-u
+[`--type`]: #-type-or-t
[`--help`]: #-help
##### `--amend`
@@ -147,6 +166,7 @@ create changelogs/unreleased/feature-hey-dz.yml
title: Added an awesome new feature to GitLab
merge_request:
author:
+type:
```
##### `--force` or `-f`
@@ -164,6 +184,7 @@ create changelogs/unreleased/feature-hey-dz.yml
title: Hey DZ, I added a feature to GitLab!
merge_request: 1983
author:
+type:
```
##### `--merge-request` or `-m`
@@ -178,6 +199,7 @@ create changelogs/unreleased/feature-hey-dz.yml
title: Hey DZ, I added a feature to GitLab!
merge_request: 1983
author:
+type:
```
##### `--dry-run` or `-n`
@@ -192,6 +214,7 @@ create changelogs/unreleased/feature-hey-dz.yml
title: Added an awesome new feature to GitLab
merge_request:
author:
+type:
$ ls changelogs/unreleased/
```
@@ -211,6 +234,21 @@ create changelogs/unreleased/feature-hey-dz.yml
title: Hey DZ, I added a feature to GitLab!
merge_request:
author: Jane Doe
+type:
+```
+
+##### `--type` or `-t`
+
+Use the **`--type`** or **`-t`** argument to provide the `type` value:
+
+```text
+$ bin/changelog 'Hey DZ, I added a feature to GitLab!' -t added
+create changelogs/unreleased/feature-hey-dz.yml
+---
+title: Hey DZ, I added a feature to GitLab!
+merge_request:
+author:
+type: added
```
### History and Reasoning
diff --git a/doc/development/database_merge_request_checklist.md b/doc/development/database_merge_request_checklist.md
new file mode 100644
index 00000000000..75c395b61ef
--- /dev/null
+++ b/doc/development/database_merge_request_checklist.md
@@ -0,0 +1,15 @@
+# Merge Request Checklist
+
+When creating a merge request that performs database related changes (schema
+changes, adjusting queries to optimise performance, etc) you should use the
+merge request template called "Database Changes". This template contains a
+checklist of steps to follow to make sure the changes are up to snuff.
+
+To use the checklist, create a new merge request and click on the "Choose a
+template" dropdown, then click "Database Changes".
+
+An example of this checklist can be found at
+https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12463.
+
+The source code of the checklist can be found in at
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/merge_request_templates/Database%20Changes.md
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 90d1d9657b9..798f40eef3d 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -113,13 +113,12 @@ merge request.
## Links
-- If a link makes the paragraph to span across multiple lines, do not use
- the regular Markdown approach: `[Text](https://example.com)`. Instead use
- `[Text][identifier]` and at the very bottom of the document add:
- `[identifier]: https://example.com`. This is another way to create Markdown
- links which keeps the document clear and concise. Bonus points if you also
- add an alternative text: `[identifier]: https://example.com "Alternative text"`
- that appears when hovering your mouse on a link
+- Use the regular inline link markdown markup `[Text](https://example.com)`.
+ It's easier to read, review, and maintain.
+- If there's a link that repeats several times through the same document,
+ you can use `[Text][identifier]` and at the bottom of the section or the
+ document add: `[identifier]: https://example.com`, in which case, we do
+ encourage you to also add an alternative text: `[identifier]: https://example.com "Alternative text"` that appears when hovering your mouse on a link.
### Linking to inline docs
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 6ade3231fac..4f20aa070de 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -102,42 +102,41 @@ followed by any global declarations, then a blank newline prior to any imports o
1. Relative paths: when importing a module in the same directory, a child
directory, or an immediate parent directory prefer relative paths. When
-importing a module which is two or more levels up, prefer either `~/` or `ee/`
-.
+importing a module which is two or more levels up, prefer either `~/` or `ee/`.
-In **app/assets/javascripts/my-feature/subdir**:
+ In **app/assets/javascripts/my-feature/subdir**:
-``` javascript
-// bad
-import Foo from '~/my-feature/foo';
-import Bar from '~/my-feature/subdir/bar';
-import Bin from '~/my-feature/subdir/lib/bin';
+ ```javascript
+ // bad
+ import Foo from '~/my-feature/foo';
+ import Bar from '~/my-feature/subdir/bar';
+ import Bin from '~/my-feature/subdir/lib/bin';
-// good
-import Foo from '../foo';
-import Bar from './bar';
-import Bin from './lib/bin';
-```
+ // good
+ import Foo from '../foo';
+ import Bar from './bar';
+ import Bin from './lib/bin';
+ ```
-In **spec/javascripts**:
+ In **spec/javascripts**:
-``` javascript
-// bad
-import Foo from '../../app/assets/javascripts/my-feature/foo';
+ ```javascript
+ // bad
+ import Foo from '../../app/assets/javascripts/my-feature/foo';
-// good
-import Foo from '~/my-feature/foo';
-```
+ // good
+ import Foo from '~/my-feature/foo';
+ ```
-When referencing an **EE component**:
+ When referencing an **EE component**:
-``` javascript
-// bad
-import Foo from '../../../../../ee/app/assets/javascripts/my-feature/ee-foo';
+ ```javascript
+ // bad
+ import Foo from '../../../../../ee/app/assets/javascripts/my-feature/ee-foo';
-// good
-import Foo from 'ee/my-feature/foo';
-```
+ // good
+ import Foo from 'ee/my-feature/foo';
+ ```
1. Avoid using IIFE. Although we have a lot of examples of files which wrap their
contents in IIFEs (immediately-invoked function expressions),
@@ -145,24 +144,23 @@ this is no longer necessary after the transition from Sprockets to webpack.
Do not use them anymore and feel free to remove them when refactoring legacy code.
1. Avoid adding to the global namespace.
- ```javascript
- // bad
- window.MyClass = class { /* ... */ };
+ ```javascript
+ // bad
+ window.MyClass = class { /* ... */ };
- // good
- export default class MyClass { /* ... */ }
- ```
+ // good
+ export default class MyClass { /* ... */ }
+ ```
1. Side effects are forbidden in any script which contains exports
- ```javascript
- // bad
- export default class MyClass { /* ... */ }
-
- document.addEventListener("DOMContentLoaded", function(event) {
- new MyClass();
- }
- ```
+ ```javascript
+ // bad
+ export default class MyClass { /* ... */ }
+ document.addEventListener("DOMContentLoaded", function(event) {
+ new MyClass();
+ }
+ ```
#### Data Mutation and Pure functions
1. Strive to write many small pure functions, and minimize where mutations occur.
@@ -414,19 +412,19 @@ A forEach will cause side effects, it will be mutating the array being iterated.
#### Data
1. `data` method should always be a function
- ```javascript
- // bad
- data: {
- foo: 'foo'
- }
-
- // good
- data() {
- return {
+ ```javascript
+ // bad
+ data: {
foo: 'foo'
- };
- }
- ```
+ }
+
+ // good
+ data() {
+ return {
+ foo: 'foo'
+ };
+ }
+ ```
#### Directives
@@ -481,7 +479,8 @@ A forEach will cause side effects, it will be mutating the array being iterated.
1. `beforeDestroy`
1. `destroyed`
-#### Vue and Boostrap
+#### Vue and Bootstrap
+
1. Tooltips: Do not rely on `has-tooltip` class name for Vue components
```javascript
// bad
@@ -512,6 +511,19 @@ A forEach will cause side effects, it will be mutating the array being iterated.
$('span').tooltip('fixTitle');
```
+### The Javascript/Vue Accord
+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. 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. 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.
+1. You may have a temporary but immediate need to create technical debt by writing code that does not follow our standards, to be refactored later. Maintainers need to be ok with the tech debt in the first place. An issue should be created for that tech debt to evaluate it further and discuss. In the coming months you should fix that tech debt, with it's priority to be determined by maintainers.
+1. When creating tech debt you must write the tests for that code before hand and those tests may not be rewritten. e.g. jQuery tests rewritten to Vue tests.
+1. You may choose to use VueX as a centralized state management. If you choose not to use VueX, you must use the *store pattern* which can be found in the [Vue.js documentation](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch).
+1. Once you have chosen a centralized state management solution you must use it for your entire application. i.e. Don't mix and match your state management solutions.
## SCSS
- [SCSS](style_guide_scss.md)
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 0742b202807..2607353782a 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -28,8 +28,9 @@ As always, the Frontend Architectural Experts are available to help with any Vue
All new features built with Vue.js must follow a [Flux architecture][flux].
The main goal we are trying to achieve is to have only one data flow and only one data entry.
-In order to achieve this goal, each Vue bundle needs a Store - where we keep all the data -,
-a Service - that we use to communicate with the server - and a main Vue component.
+In order to achieve this goal, you can either use [vuex](#vuex) or use the [store pattern][state-management], explained below:
+
+Each Vue bundle needs a Store - where we keep all the data -,a Service - that we use to communicate with the server - and a main Vue component.
Think of the Main Vue Component as the entry point of your application. This is the only smart
component that should exist in each Vue feature.
@@ -74,6 +75,59 @@ provided as a prop to the main component.
Don't forget to follow [these steps.][page_specific_javascript]
+### Bootstrapping Gotchas
+#### Providing data from Haml to JavaScript
+While mounting a Vue application may be a need to provide data from Rails to JavaScript.
+To do that, provide the data through `data` attributes in the HTML element and query them while mounting the application.
+
+_Note:_ You should only do this while initing the application, because the mounted element will be replaced with Vue-generated DOM.
+
+The advantage of providing data from the DOM to the Vue instance through `props` in the `render` function
+instead of querying the DOM inside the main vue component is that makes tests easier by avoiding the need to
+create a fixture or an HTML element in the unit test. See the following example:
+
+```javascript
+// haml
+.js-vue-app{ data: { endpoint: 'foo' }}
+
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '.js-vue-app',
+ data() {
+ const dataset = this.$options.el.dataset;
+ return {
+ endpoint: dataset.endpoint,
+ };
+ },
+ render(createElement) {
+ return createElement('my-component', {
+ props: {
+ endpoint: this.isLoading,
+ },
+ });
+ },
+}));
+```
+
+#### Accessing the `gl` object
+When we need to query the `gl` object for data that won't change during the application's lyfecyle, we should do it in the same place where we query the DOM.
+By following this practice, we can avoid the need to mock the `gl` object, which will make tests easier.
+It should be done while initializing our Vue instance, and the data should be provided as `props` to the main component:
+
+##### example:
+```javascript
+
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '.js-vue-app',
+ render(createElement) {
+ return createElement('my-component', {
+ props: {
+ username: gon.current_username,
+ },
+ });
+ },
+}));
+```
+
### A folder for Components
This folder holds all components that are specific of this new feature.
@@ -89,6 +143,29 @@ in one table would not be a good use of this pattern.
You can read more about components in Vue.js site, [Component System][component-system]
+#### Components Gotchas
+1. Using SVGs in components: To use an SVG in a template we need to make it a property we can access through the component.
+A `prop` and a property returned by the `data` functions require `vue` to set a `getter` and a `setter` for each of them.
+The SVG should be a computed property in order to improve performance, note that computed properties are cached based on their dependencies.
+
+```javascript
+// bad
+import svg from 'svg.svg';
+data() {
+ return {
+ myIcon: svg,
+ };
+};
+
+// good
+import svg from 'svg.svg';
+computed: {
+ myIcon() {
+ return svg;
+ }
+}
+```
+
### A folder for the Store
The Store is a class that allows us to manage the state in a single
@@ -430,11 +507,23 @@ describe('Todos App', () => {
});
});
```
+#### `mountComponent` helper
+There is an helper in `spec/javascripts/helpers/vue_mount_component_helper.js` that allows you to mount a component with the given props:
+
+```javascript
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper.js'
+import component from 'component.vue'
+
+const Component = Vue.extend(component);
+const data = {prop: 'foo'};
+const vm = mountComponent(Component, data);
+```
+
#### Test the component's output
The main return value of a Vue component is the rendered output. In order to test the component we
need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that:
-
### Stubbing API responses
[Vue Resource Interceptors][vue-resource-interceptor] allow us to add a interceptor with
the response we need:
@@ -481,6 +570,198 @@ new Component({
new Component().$mount();
```
+## Vuex
+To manage the state of an application you may use [Vuex][vuex-docs].
+
+_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs].
+
+### Separation of concerns
+Vuex is composed of State, Getters, Mutations, Actions and Modules.
+
+When a user clicks on an action, we need to `dispatch` it. This action will `commit` a mutation that will change the state.
+_Note:_ The action itself will not update the state, only a mutation should update the state.
+
+#### File structure
+When using Vuex at GitLab, separate this concerns into different files to improve readability. If you can, separate the Mutation Types as well:
+
+```
+└── store
+ ├── index.js # where we assemble modules and export the store
+ ├── actions.js # actions
+ ├── mutations.js # mutations
+ ├── getters.js # getters
+ └── mutation_types.js # mutation types
+```
+The following examples show an application that lists and adds users to the state.
+
+##### `index.js`
+This is the entry point for our store. You can use the following as a guide:
+
+```javascript
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as mutations from './mutations';
+
+Vue.use(Vuex);
+
+export default new Vuex.Store({
+ actions,
+ getters,
+ state: {
+ users: [],
+ },
+});
+```
+_Note:_ If the state of the application is too complex, an individual file for the state may be better.
+
+#### `actions.js`
+An action commits a mutatation. In this file, we will write the actions that will call the respective mutation:
+
+```javascript
+ import * as types from './mutation-types'
+
+ export const addUser = ({ commit }, user) => {
+ commit(types.ADD_USER, user);
+ };
+```
+
+To dispatch an action from a component, use the `mapActions` helper:
+```javascript
+import { mapActions } from 'vuex';
+
+{
+ methods: {
+ ...mapActions([
+ 'addUser',
+ ]),
+ onClickUser(user) {
+ this.addUser(user);
+ },
+ },
+};
+```
+
+#### `getters.js`
+Sometimes we may need to get derived state based on store state, like filtering for a specific prop. This can be done through the `getters`:
+
+```javascript
+// get all the users with pets
+export getUsersWithPets = (state, getters) => {
+ return state.users.filter(user => user.pet !== undefined);
+};
+```
+
+To access a getter from a component, use the `mapGetters` helper:
+```javascript
+import { mapGetters } from 'vuex';
+
+{
+ computed: {
+ ...mapGetters([
+ 'getUsersWithPets',
+ ]),
+ },
+};
+```
+
+#### `mutations.js`
+The only way to actually change state in a Vuex store is by committing a mutation.
+
+```javascript
+ import * as types from './mutation-types'
+ export default {
+ [types.ADD_USER](state, user) {
+ state.users.push(user);
+ },
+ };
+```
+
+#### `mutations_types.js`
+From [vuex mutations docs][vuex-mutations]:
+> It is a commonly seen pattern to use constants for mutation types in various Flux implementations. This allows the code to take advantage of tooling like linters, and putting all constants in a single file allows your collaborators to get an at-a-glance view of what mutations are possible in the entire application.
+
+```javascript
+export const ADD_USER = 'ADD_USER';
+```
+
+### How to include the store in your application
+The store should be included in the main component of your application:
+```javascript
+ // app.vue
+ import store from 'store'; // it will include the index.js file
+
+ export default {
+ name: 'application',
+ store,
+ ...
+ };
+```
+
+### Vuex Gotchas
+1. Avoid calling a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency through out the application. From Vuex docs:
+
+ > why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action.
+
+ ```javascript
+ // component.vue
+
+ // bad
+ created() {
+ this.$store.commit('mutation');
+ }
+
+ // good
+ created() {
+ this.$store.dispatch('action');
+ }
+ ```
+1. When possible, use mutation types instead of hardcoding strings. It will be less error prone.
+1. The State will be accessible in all components descending from the use where the store is instantiated.
+
+### Testing Vuex
+#### Testing Vuex concerns
+Refer to [vuex docs][vuex-testing] regarding testing Actions, Getters and Mutations.
+
+#### Testing components that need a store
+Smaller components might use `store` properties to access the data.
+In order to write unit tests for those components, we need to include the store and provide the correct state:
+
+```javascript
+//component_spec.js
+import Vue from 'vue';
+import store from './store';
+import component from './component.vue'
+
+describe('component', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(issueActions);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should show a user', () => {
+ const user = {
+ name: 'Foo',
+ age: '30',
+ };
+
+ // populate the store
+ store.dipatch('addUser', user);
+
+ vm = new Component({
+ store,
+ propsData: props,
+ }).$mount();
+ });
+});
+```
+
[vue-docs]: http://vuejs.org/guide/index.html
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
@@ -493,3 +774,7 @@ new Component().$mount();
[vue-test]: https://vuejs.org/v2/guide/unit-testing.html
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
[flux]: https://facebook.github.io/flux
+[vuex-docs]: https://vuex.vuejs.org
+[vuex-structure]: https://vuex.vuejs.org/en/structure.html
+[vuex-mutations]: https://vuex.vuejs.org/en/mutations.html
+[vuex-testing]: https://vuex.vuejs.org/en/testing.html
diff --git a/doc/development/hash_indexes.md b/doc/development/hash_indexes.md
new file mode 100644
index 00000000000..e6c1b3590b1
--- /dev/null
+++ b/doc/development/hash_indexes.md
@@ -0,0 +1,20 @@
+# Hash Indexes
+
+Both PostgreSQL and MySQL support hash indexes besides the regular btree
+indexes. Hash indexes however are to be avoided at all costs. While they may
+_sometimes_ provide better performance the cost of rehashing can be very high.
+More importantly: at least until PostgreSQL 10.0 hash indexes are not
+WAL-logged, meaning they are not replicated to any replicas. From the PostgreSQL
+documentation:
+
+> Hash index operations are not presently WAL-logged, so hash indexes might need
+> to be rebuilt with REINDEX after a database crash if there were unwritten
+> changes. Also, changes to hash indexes are not replicated over streaming or
+> file-based replication after the initial base backup, so they give wrong
+> answers to queries that subsequently use them. For these reasons, hash index
+> use is presently discouraged.
+
+RuboCop is configured to register an offence when it detects the use of a hash
+index.
+
+Instead of using hash indexes you should use regular btree indexes.
diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md
index 756535e28bc..bd0ef39ca62 100644
--- a/doc/development/i18n_guide.md
+++ b/doc/development/i18n_guide.md
@@ -138,6 +138,47 @@ 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
diff --git a/doc/development/img/manual_build_docs.png b/doc/development/img/manual_build_docs.png
new file mode 100644
index 00000000000..fef767c2a79
--- /dev/null
+++ b/doc/development/img/manual_build_docs.png
Binary files differ
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index 1f115059fb8..9a5811d8474 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -55,6 +55,7 @@ Libraries with the following licenses are acceptable for use:
- [BSD 3-Clause License][BSD-3-Clause] (also known as New BSD or Modified BSD): A permissive (non-copyleft) license as defined by the Open Source Initiative
- [ISC License][ISC] (also known as the OpenBSD License): A permissive (non-copyleft) license as defined by the Open Source Initiative.
- [Creative Commons Zero (CC0)][CC0]: A public domain dedication, recommended as a way to disclaim copyright on your work to the maximum extent possible.
+- [Unlicense][UNLICENSE]: Another public domain dedication.
## Unacceptable Licenses
@@ -63,10 +64,11 @@ Libraries with the following licenses are unacceptable for use:
- [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects.
- [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.
## Requesting Approval for Licenses
-Libraries that are not listed in the [Acceptable Licenses][Acceptable-Licenses] or [Unacceptable Licenses][Unacceptable-Licenses] list can be submitted to the legal team for review. Please create an issue in the [Organization Repository][Org-Repo] and cc `@gl-legal`. After a decision has been made, the original requestor is responsible for updating this document.
+Libraries that are not listed in the [Acceptable Licenses][Acceptable-Licenses] or [Unacceptable Licenses][Unacceptable-Licenses] list can be submitted to the legal team for review. Please email `legal@gitlab.com` with the details. After a decision has been made, the original requestor is responsible for updating this document.
## Notes
@@ -101,5 +103,8 @@ Gems which are included only in the "development" or "test" groups by Bundler ar
[OSL]: https://opensource.org/licenses/OSL-3.0
[OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL
[Org-Repo]: https://gitlab.com/gitlab-com/organization
+[UNLICENSE]: https://unlicense.org
+[Facebook]: https://code.facebook.com/pages/850928938376556
+[x-list]: https://www.apache.org/legal/resolved.html#category-x
[Acceptable-Licenses]: #acceptable-licenses
[Unacceptable-Licenses]: #unacceptable-licenses
diff --git a/doc/development/ordering_table_columns.md b/doc/development/ordering_table_columns.md
new file mode 100644
index 00000000000..249e70c7b0e
--- /dev/null
+++ b/doc/development/ordering_table_columns.md
@@ -0,0 +1,127 @@
+# Ordering Table Columns
+
+Similar to C structures the space of a table is influenced by the order of
+columns. This is because the size of columns is aligned depending on the type of
+the column. Take the following column order for example:
+
+* id (integer, 4 bytes)
+* name (text, variable)
+* user_id (integer, 4 bytes)
+
+Integers are aligned to the word size. This means that on a 64 bit platform the
+actual size of each column would be: 8 bytes, variable, 8 bytes. This means that
+each row will require at least 16 bytes for the two integers, and a variable
+amount for the text field. If a table has a few rows this is not an issue, but
+once you start storing millions of rows you can save space by using a different
+order. For the above example a more ideal column order would be the following:
+
+* id (integer, 4 bytes)
+* user_id (integer, 4 bytes)
+* name (text, variable)
+
+In this setup the `id` and `user_id` columns can be packed together, which means
+we only need 8 bytes to store _both_ of them. This in turn each row will require
+8 bytes less of space.
+
+For GitLab we require that columns of new tables are ordered based to use the
+least amount of space. An easy way of doing this is to order them based on the
+type size in descending order with variable sizes (string and text columns for
+example) at the end.
+
+## Type Sizes
+
+While the PostgreSQL docuemntation
+(https://www.postgresql.org/docs/current/static/datatype.html) contains plenty
+of information we will list the sizes of common types here so it's easier to
+look them up. Here "word" refers to the word size, which is 4 bytes for a 32
+bits platform and 8 bytes for a 64 bits platform.
+
+| Type | Size | Aligned To |
+|:-----------------|:-------------------------------------|:-----------|
+| smallint | 2 bytes | 1 word |
+| integer | 4 bytes | 1 word |
+| bigint | 8 bytes | 8 bytes |
+| real | 4 bytes | 1 word |
+| double precision | 8 bytes | 8 bytes |
+| boolean | 1 byte | not needed |
+| text / string | variable, 1 byte plus the data | 1 word |
+| bytea | variable, 1 or 4 bytes plus the data | 1 word |
+| timestamp | 8 bytes | 8 bytes |
+| timestamptz | 8 bytes | 8 bytes |
+| date | 4 bytes | 1 word |
+
+A "variable" size means the actual size depends on the value being stored. If
+PostgreSQL determines this can be embedded directly into a row it may do so, but
+for very large values it will store the data externally and store a pointer (of
+1 word in size) in the column. Because of this variable sized columns should
+always be at the end of a table.
+
+## Real Example
+
+Let's use the "events" table as an example, which currently has the following
+layout:
+
+| Column | Type | Size |
+|:------------|:----------------------------|:---------|
+| id | integer | 4 bytes |
+| target_type | character varying | variable |
+| target_id | integer | 4 bytes |
+| title | character varying | variable |
+| data | text | variable |
+| project_id | integer | 4 bytes |
+| created_at | timestamp without time zone | 8 bytes |
+| updated_at | timestamp without time zone | 8 bytes |
+| action | integer | 4 bytes |
+| author_id | integer | 4 bytes |
+
+After adding padding to align the columns this would translate to columns being
+divided into fixed size chunks as follows:
+
+| Chunk Size | Columns |
+|:-----------|:------------------|
+| 8 bytes | id |
+| variable | target_type |
+| 8 bytes | target_id |
+| variable | title |
+| variable | data |
+| 8 bytes | project_id |
+| 8 bytes | created_at |
+| 8 bytes | updated_at |
+| 8 bytes | action, author_id |
+
+This means that excluding the variable sized data we need at least 48 bytes per
+row.
+
+We can optimise this by using the following column order instead:
+
+| Column | Type | Size |
+|:------------|:----------------------------|:---------|
+| created_at | timestamp without time zone | 8 bytes |
+| updated_at | timestamp without time zone | 8 bytes |
+| id | integer | 4 bytes |
+| target_id | integer | 4 bytes |
+| project_id | integer | 4 bytes |
+| action | integer | 4 bytes |
+| author_id | integer | 4 bytes |
+| target_type | character varying | variable |
+| title | character varying | variable |
+| data | text | variable |
+
+This would produce the following chunks:
+
+| Chunk Size | Columns |
+|:-----------|:-------------------|
+| 8 bytes | created_at |
+| 8 bytes | updated_at |
+| 8 bytes | id, target_id |
+| 8 bytes | project_id, action |
+| 8 bytes | author_id |
+| variable | target_type |
+| variable | title |
+| variable | data |
+
+Here we only need 40 bytes per row excluding the variable sized data. 8 bytes
+being saved may not sound like much, but for tables as large as the "events"
+table it does begin to matter. For example, when storing 80 000 000 rows this
+translates to a space saving of at least 610 MB: all by just changing the order
+of a few columns.
diff --git a/doc/development/serializing_data.md b/doc/development/serializing_data.md
index 2b56f48bc44..37332c20147 100644
--- a/doc/development/serializing_data.md
+++ b/doc/development/serializing_data.md
@@ -1,7 +1,8 @@
# Serializing Data
**Summary:** don't store serialized data in the database, use separate columns
-and/or tables instead.
+and/or tables instead. This includes storing of comma separated values as a
+string.
Rails makes it possible to store serialized data in JSON, YAML or other formats.
Such a field can be defined as follows:
diff --git a/doc/development/sql.md b/doc/development/sql.md
index 23fd7604957..974b1d99dff 100644
--- a/doc/development/sql.md
+++ b/doc/development/sql.md
@@ -216,4 +216,30 @@ exact same results. This also means there's no need to add an index on
`created_at` to ensure consistent performance as `id` is already indexed by
default.
+## Use WHERE EXISTS instead of WHERE IN
+
+While `WHERE IN` and `WHERE EXISTS` can be used to produce the same data it is
+recommended to use `WHERE EXISTS` whenever possible. While in many cases
+PostgreSQL can optimise `WHERE IN` quite well there are also many cases where
+`WHERE EXISTS` will perform (much) better.
+
+In Rails you have to use this by creating SQL fragments:
+
+```ruby
+Project.where('EXISTS (?)', User.select(1).where('projects.creator_id = users.id AND users.foo = X'))
+```
+
+This would then produce a query along the lines of the following:
+
+```sql
+SELECT *
+FROM projects
+WHERE EXISTS (
+ SELECT 1
+ FROM users
+ WHERE projects.creator_id = users.id
+ AND users.foo = X
+)
+```
+
[gin-index]: http://www.postgresql.org/docs/current/static/gin.html
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 3d5aa3d45e9..83269303005 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -157,8 +157,9 @@ trade-off:
- Unit tests are usually cheap, and you should consider them like the basement
of your house: you need them to be confident that your code is behaving
- correctly. However if you run only unit tests without integration / system tests, you might [miss] the [big] [picture]!
-- Integration tests are a bit more expensive, but don't abuse them. A feature test
+ correctly. However if you run only unit tests without integration / system
+ tests, you might [miss] the [big] [picture]!
+- Integration tests are a bit more expensive, but don't abuse them. A system test
is often better than an integration test that is stubbing a lot of internals.
- System tests are expensive (compared to unit tests), even more if they require
a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed)
@@ -188,24 +189,34 @@ Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md).
### General Guidelines
- Use a single, top-level `describe ClassName` block.
-- Use `described_class` instead of repeating the class name being described
- (_this is enforced by RuboCop_).
- Use `.method` to describe class methods and `#method` to describe instance
methods.
- Use `context` to test branching logic.
-- Use multi-line `do...end` blocks for `before` and `after`, even when it would
- fit on a single line.
- Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
-- Don't supply the `:each` argument to hooks since it's the default.
-- Prefer `not_to` to `to_not` (_this is enforced by RuboCop_).
- Try to match the ordering of tests to the ordering within the class.
- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases.
-- Try to use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
+- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
+- Don't assert against the absolute value of a sequence-generated attribute (see
+ [Gotchas](gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
+- Don't supply the `:each` argument to hooks since it's the default.
- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
[four-phase-test]: https://robots.thoughtbot.com/four-phase-test
+### Automatic retries and flaky tests detection
+
+On our CI, we use [rspec-retry] to automatically retry a failing example a few
+times (see [`spec/spec_helper.rb`] for the precise retries count).
+
+We also use a home-made `RspecFlaky::Listener` listener which records flaky
+examples in a JSON report file on `master` (`retrieve-tests-metadata` and `update-tests-metadata` jobs), and warns when a new flaky example
+is detected in any other branch (`flaky-examples-check` job). In the future, the
+`flaky-examples-check` job will not be allowed to fail.
+
+[rspec-retry]: https://github.com/NoRedInk/rspec-retry
+[`spec/spec_helper.rb`]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/spec_helper.rb
+
### `let` variables
GitLab's RSpec suite has made extensive use of `let` variables to reduce
@@ -268,6 +279,43 @@ end
- Avoid scenario titles that add no information, such as "successfully".
- Avoid scenario titles that repeat the feature title.
+### Table-based / Parameterized tests
+
+This style of testing is used to exercise one piece of code with a comprehensive
+range of inputs. By specifying the test case once, alongside a table of inputs
+and the expected output for each, your tests can be made easier to read and more
+compact.
+
+We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized)
+gem. A short example, using the table syntax and checking Ruby equality for a
+range of inputs, might look like this:
+
+```ruby
+describe "#==" do
+ using Rspec::Parameterized::TableSyntax
+
+ let(:project1) { create(:project) }
+ let(:project2) { create(:project) }
+ where(:a, :b, :result) do
+ 1 | 1 | true
+ 1 | 2 | false
+ true | true | true
+ true | false | false
+ project1 | project1 | true
+ project2 | project2 | true
+ project 1 | project2 | false
+ end
+
+ with_them do
+ it { expect(a == b).to eq(result) }
+
+ it 'is isomorphic' do
+ expect(b == a).to eq(result)
+ end
+ end
+end
+```
+
### Matchers
Custom matchers should be created to clarify the intent and/or hide the
@@ -276,6 +324,15 @@ complexity of RSpec expectations.They should be placed under
a certain type of specs only (e.g. features, requests etc.) but shouldn't be if
they apply to multiple type of specs.
+#### have_gitlab_http_status
+
+Prefer `have_gitlab_http_status` over `have_http_status` because the former
+could also show the response body whenever the status mismatched. This would
+be very useful whenever some tests start breaking and we would love to know
+why without editing the source and rerun the tests.
+
+This is especially useful whenever it's showing 500 internal server error.
+
### Shared contexts
All shared contexts should be be placed under `spec/support/shared_contexts/`.
@@ -472,10 +529,7 @@ slowest test files and try to improve them.
## CI setup
-- On CE, the test suite only runs against PostgreSQL by default. We additionally
- run the suite against MySQL for tags, `master`, and any branch that includes
- `mysql` in the name.
-- On EE, the test suite always runs both PostgreSQL and MySQL.
+- On CE and EE, the test suite runs both PostgreSQL and MySQL.
- Rails logging to `log/test.log` is disabled by default in CI [for
performance reasons][logging]. To override this setting, provide the
`RAILS_ENABLE_TEST_LOG` environment variable.
diff --git a/doc/development/verifying_database_capabilities.md b/doc/development/verifying_database_capabilities.md
new file mode 100644
index 00000000000..cc6d62957e3
--- /dev/null
+++ b/doc/development/verifying_database_capabilities.md
@@ -0,0 +1,26 @@
+# Verifying Database Capabilities
+
+Sometimes certain bits of code may only work on a certain database and/or
+version. While we try to avoid such code as much as possible sometimes it is
+necessary to add database (version) specific behaviour.
+
+To facilitate this we have the following methods that you can use:
+
+* `Gitlab::Database.postgresql?`: returns `true` if PostgreSQL is being used
+* `Gitlab::Database.mysql?`: returns `true` if MySQL is being used
+* `Gitlab::Database.version`: returns the PostgreSQL version number as a string
+ in the format `X.Y.Z`. This method does not work for MySQL
+
+This allows you to write code such as:
+
+```ruby
+if Gitlab::Database.postgresql?
+ if Gitlab::Database.version.to_f >= 9.6
+ run_really_fast_query
+ else
+ run_fast_query
+ end
+else
+ run_query
+end
+```
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
index eac9ec2a470..479258f743e 100644
--- a/doc/development/writing_documentation.md
+++ b/doc/development/writing_documentation.md
@@ -103,3 +103,24 @@ If that job fails, read the instructions in the job log for what to do next.
Contributors do not need to submit their changes to EE, GitLab Inc. employees
on the other hand need to make sure that their changes apply cleanly to both
CE and EE.
+
+## Previewing the changes live
+
+If you want to preview your changes live, you can use the manual `build-docs`
+job in your merge request.
+
+![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:**
+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.
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index 2513f4b420a..67ef189fee9 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -1,5 +1,9 @@
# How to create a project in GitLab
+>**Notes:**
+- For a list of words that are not allowed to be used as project names see the
+ [reserved names][reserved].
+
1. In your dashboard, click the green **New project** button or use the plus
icon in the upper right corner of the navigation bar.
@@ -9,13 +13,17 @@
![Project information](img/create_new_project_info.png)
+1. Choose if you want start a blank project, or with one of the predefined
+ [Project Templates](https://gitlab.com/gitlab-org/project-templates):
+ this will kickstart your repository code and CI automatically.
+ Otherwise, if you have a project in a different repository, you can [import it] by
+ clicking an **Import project from** button provided this is enabled in
+ your GitLab instance. Ask your administrator if not.
+
1. Provide the following information:
- Enter the name of your project in the **Project name** field. You can't use
special characters, but you can use spaces, hyphens, underscores or even
emoji.
- - If you have a project in a different repository, you can [import it] by
- clicking an **Import project from** button provided this is enabled in
- your GitLab instance. Ask your administrator if not.
- The **Project description (optional)** field enables you to enter a
description for your project's dashboard, which will help others
understand what your project is about. Though it's not required, it's a good
@@ -26,3 +34,4 @@
1. Click **Create project**.
[import it]: ../workflow/importing/README.md
+[reserved]: ../user/reserved_names.md
diff --git a/doc/gitlab-basics/img/create_new_project_info.png b/doc/gitlab-basics/img/create_new_project_info.png
index fcfbca87b91..ef8753e224b 100644
--- a/doc/gitlab-basics/img/create_new_project_info.png
+++ b/doc/gitlab-basics/img/create_new_project_info.png
Binary files differ
diff --git a/doc/install/README.md b/doc/install/README.md
index bc831a37735..656f8720361 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -17,10 +17,18 @@ the hardware requirements.
- [Installation from source](installation.md) - Install GitLab from source.
Useful for unsupported systems like *BSD. For an overview of the directory
structure, read the [structure documentation](structure.md).
-- [Docker](https://docs.gitlab.com/omnibus/docker/) - Install GitLab using Docker.
+- [Docker](docker.md) - Install GitLab using Docker.
+
+## Install GitLab on cloud providers
+
- [Installing in Kubernetes](kubernetes/index.md) - Install GitLab into a Kubernetes
Cluster using our official Helm Chart Repository.
-- Testing only! [DigitalOcean and Docker Machine](digitaloceandocker.md) -
+- [Install GitLab on OpenShift](../articles/openshift_and_gitlab/index.md)
+- [Install GitLab on DC/OS](https://mesosphere.com/blog/gitlab-dcos/) via [GitLab-Mesosphere integration](https://about.gitlab.com/2016/09/16/announcing-gitlab-and-mesosphere/)
+- [Install GitLab on Azure](azure/index.md)
+- [Install GitLab on Google Cloud Platform](google_cloud_platform/index.md)
+- [Install on AWS](https://about.gitlab.com/aws/)
+- _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md) -
Quickly test any version of GitLab on DigitalOcean using Docker Machine.
## Database
diff --git a/doc/install/azure/img/azure-add-inbound-sec-rule-http.png b/doc/install/azure/img/azure-add-inbound-sec-rule-http.png
new file mode 100644
index 00000000000..abf500cb63a
--- /dev/null
+++ b/doc/install/azure/img/azure-add-inbound-sec-rule-http.png
Binary files differ
diff --git a/doc/install/azure/img/azure-add-inbound-sec-rule-ssh.png b/doc/install/azure/img/azure-add-inbound-sec-rule-ssh.png
new file mode 100644
index 00000000000..f7a8a04dfa7
--- /dev/null
+++ b/doc/install/azure/img/azure-add-inbound-sec-rule-ssh.png
Binary files differ
diff --git a/doc/install/azure/img/azure-create-virtual-machine-basics-password.png b/doc/install/azure/img/azure-create-virtual-machine-basics-password.png
new file mode 100644
index 00000000000..80bf39ecb48
--- /dev/null
+++ b/doc/install/azure/img/azure-create-virtual-machine-basics-password.png
Binary files differ
diff --git a/doc/install/azure/img/azure-create-virtual-machine-basics.png b/doc/install/azure/img/azure-create-virtual-machine-basics.png
new file mode 100644
index 00000000000..229c073fe17
--- /dev/null
+++ b/doc/install/azure/img/azure-create-virtual-machine-basics.png
Binary files differ
diff --git a/doc/install/azure/img/azure-create-virtual-machine-deployment.png b/doc/install/azure/img/azure-create-virtual-machine-deployment.png
new file mode 100644
index 00000000000..5cfdd973a58
--- /dev/null
+++ b/doc/install/azure/img/azure-create-virtual-machine-deployment.png
Binary files differ
diff --git a/doc/install/azure/img/azure-create-virtual-machine-purchase.png b/doc/install/azure/img/azure-create-virtual-machine-purchase.png
new file mode 100644
index 00000000000..f4de62299f1
--- /dev/null
+++ b/doc/install/azure/img/azure-create-virtual-machine-purchase.png
Binary files differ
diff --git a/doc/install/azure/img/azure-create-virtual-machine-settings.png b/doc/install/azure/img/azure-create-virtual-machine-settings.png
new file mode 100644
index 00000000000..20097921660
--- /dev/null
+++ b/doc/install/azure/img/azure-create-virtual-machine-settings.png
Binary files differ
diff --git a/doc/install/azure/img/azure-create-virtual-machine-size.png b/doc/install/azure/img/azure-create-virtual-machine-size.png
new file mode 100644
index 00000000000..a408394151f
--- /dev/null
+++ b/doc/install/azure/img/azure-create-virtual-machine-size.png
Binary files differ
diff --git a/doc/install/azure/img/azure-dashboard-highlight-nsg.png b/doc/install/azure/img/azure-dashboard-highlight-nsg.png
new file mode 100644
index 00000000000..025efd33977
--- /dev/null
+++ b/doc/install/azure/img/azure-dashboard-highlight-nsg.png
Binary files differ
diff --git a/doc/install/azure/img/azure-dashboard-running-resources.png b/doc/install/azure/img/azure-dashboard-running-resources.png
new file mode 100644
index 00000000000..7e661a6aa65
--- /dev/null
+++ b/doc/install/azure/img/azure-dashboard-running-resources.png
Binary files differ
diff --git a/doc/install/azure/img/azure-dashboard.png b/doc/install/azure/img/azure-dashboard.png
new file mode 100644
index 00000000000..375ec8622b8
--- /dev/null
+++ b/doc/install/azure/img/azure-dashboard.png
Binary files differ
diff --git a/doc/install/azure/img/azure-inbound-sec-rules-list.png b/doc/install/azure/img/azure-inbound-sec-rules-list.png
new file mode 100644
index 00000000000..1667671b21d
--- /dev/null
+++ b/doc/install/azure/img/azure-inbound-sec-rules-list.png
Binary files differ
diff --git a/doc/install/azure/img/azure-new-gitlab-ce.png b/doc/install/azure/img/azure-new-gitlab-ce.png
new file mode 100644
index 00000000000..88949f69a94
--- /dev/null
+++ b/doc/install/azure/img/azure-new-gitlab-ce.png
Binary files differ
diff --git a/doc/install/azure/img/azure-new-search-gitlab.png b/doc/install/azure/img/azure-new-search-gitlab.png
new file mode 100644
index 00000000000..f96ed577d62
--- /dev/null
+++ b/doc/install/azure/img/azure-new-search-gitlab.png
Binary files differ
diff --git a/doc/install/azure/img/azure-nsg-inbound-sec-rules-add-highlight.png b/doc/install/azure/img/azure-nsg-inbound-sec-rules-add-highlight.png
new file mode 100644
index 00000000000..c9a08b87ce6
--- /dev/null
+++ b/doc/install/azure/img/azure-nsg-inbound-sec-rules-add-highlight.png
Binary files differ
diff --git a/doc/install/azure/img/azure-nsg-inbound-sec-rules-highlight.png b/doc/install/azure/img/azure-nsg-inbound-sec-rules-highlight.png
new file mode 100644
index 00000000000..6423625ca8b
--- /dev/null
+++ b/doc/install/azure/img/azure-nsg-inbound-sec-rules-highlight.png
Binary files differ
diff --git a/doc/install/azure/img/azure-vm-domain-name.png b/doc/install/azure/img/azure-vm-domain-name.png
new file mode 100644
index 00000000000..01c03004b24
--- /dev/null
+++ b/doc/install/azure/img/azure-vm-domain-name.png
Binary files differ
diff --git a/doc/install/azure/img/azure-vm-management-public-ip.png b/doc/install/azure/img/azure-vm-management-public-ip.png
new file mode 100644
index 00000000000..ef313641db8
--- /dev/null
+++ b/doc/install/azure/img/azure-vm-management-public-ip.png
Binary files differ
diff --git a/doc/install/azure/img/azure-vm-management-settings-network-interfaces.png b/doc/install/azure/img/azure-vm-management-settings-network-interfaces.png
new file mode 100644
index 00000000000..4ff10718059
--- /dev/null
+++ b/doc/install/azure/img/azure-vm-management-settings-network-interfaces.png
Binary files differ
diff --git a/doc/install/azure/img/azure-vm-management.png b/doc/install/azure/img/azure-vm-management.png
new file mode 100644
index 00000000000..a0e0067258c
--- /dev/null
+++ b/doc/install/azure/img/azure-vm-management.png
Binary files differ
diff --git a/doc/install/azure/img/gitlab-admin-area-9.4.0.png b/doc/install/azure/img/gitlab-admin-area-9.4.0.png
new file mode 100644
index 00000000000..b7ee4598731
--- /dev/null
+++ b/doc/install/azure/img/gitlab-admin-area-9.4.0.png
Binary files differ
diff --git a/doc/install/azure/img/gitlab-admin-area.png b/doc/install/azure/img/gitlab-admin-area.png
new file mode 100644
index 00000000000..028f0b0a0eb
--- /dev/null
+++ b/doc/install/azure/img/gitlab-admin-area.png
Binary files differ
diff --git a/doc/install/azure/img/gitlab-change-password.png b/doc/install/azure/img/gitlab-change-password.png
new file mode 100644
index 00000000000..7350d604d56
--- /dev/null
+++ b/doc/install/azure/img/gitlab-change-password.png
Binary files differ
diff --git a/doc/install/azure/img/gitlab-home.png b/doc/install/azure/img/gitlab-home.png
new file mode 100644
index 00000000000..7c935805ea2
--- /dev/null
+++ b/doc/install/azure/img/gitlab-home.png
Binary files differ
diff --git a/doc/install/azure/img/gitlab-login.png b/doc/install/azure/img/gitlab-login.png
new file mode 100644
index 00000000000..95fe5aec1e0
--- /dev/null
+++ b/doc/install/azure/img/gitlab-login.png
Binary files differ
diff --git a/doc/install/azure/img/gitlab-new-project.png b/doc/install/azure/img/gitlab-new-project.png
new file mode 100644
index 00000000000..3b6b9a81682
--- /dev/null
+++ b/doc/install/azure/img/gitlab-new-project.png
Binary files differ
diff --git a/doc/install/azure/img/gitlab-project-home-empty.png b/doc/install/azure/img/gitlab-project-home-empty.png
new file mode 100644
index 00000000000..54d0b36a251
--- /dev/null
+++ b/doc/install/azure/img/gitlab-project-home-empty.png
Binary files differ
diff --git a/doc/install/azure/img/gitlab-project-home-instructions.png b/doc/install/azure/img/gitlab-project-home-instructions.png
new file mode 100644
index 00000000000..fd040d33e95
--- /dev/null
+++ b/doc/install/azure/img/gitlab-project-home-instructions.png
Binary files differ
diff --git a/doc/install/azure/img/gitlab-ssh-update-in-progress.png b/doc/install/azure/img/gitlab-ssh-update-in-progress.png
new file mode 100644
index 00000000000..8a686ebde26
--- /dev/null
+++ b/doc/install/azure/img/gitlab-ssh-update-in-progress.png
Binary files differ
diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md
new file mode 100644
index 00000000000..9cc4b56c932
--- /dev/null
+++ b/doc/install/azure/index.md
@@ -0,0 +1,439 @@
+# Install GitLab on Microsoft Azure
+
+> _This article was originally written by Dave Wentzel and [published on the GitLab Blog][Original-Blog-Post]._
+>
+> _Ported to the GitLab documentation and updated on 2017-08-24 by [Ian Scorer](https://gitlab.com/iscorer)._
+
+Azure is Microsoft's business cloud and GitLab is a pre-configured offering on the Azure Marketplace.
+Hopefully, you aren't surprised to hear that Microsoft and Azure have embraced open source software
+like Ubuntu, Red Hat Enterprise Linux, and of course - GitLab! This means that you can spin up a
+pre-configured GitLab VM and have your very own private GitLab up and running in around 30 minutes.
+Let's get started.
+
+## Getting started
+
+First, you'll need an account on Azure. There are three ways to do this:
+
+- If your company (or you) already has an account, then you are ready to go!
+- You can also open your own Azure account for free. _At time of writing_, you get $200
+of credit to spend on Azure services for 30 days. You can use this credit to try out paid Azure
+services, exploring Microsoft's cloud for free. Even after the first 30 days, you never have to pay
+anything unless you decide to transition to paid services with a Pay-As-You-Go Azure subscription.
+This is a great way to try out Azure and cloud computing, and you can
+[read more in their comprehensive FAQ][Azure-Free-Account-FAQ].
+- If you have an MSDN subscription, you can activate your Azure subscriber benefits. Your MSDN
+subscription gives you recurring Azure credits every month, so why not put those credits to use and
+try out GitLab right now?
+
+## Working with Azure
+
+Once you have an Azure account, you can get started. Login to Azure using
+[portal.azure.com](https://portal.azure.com) and the first thing you will see is the Dashboard:
+
+![Azure Dashboard](img/azure-dashboard.png)
+
+The Dashboard gives you a quick overview of Azure resources, and from here you you can build VMs,
+create SQL Databases, author websites, and perform lots of other cloud tasks.
+
+## Create New VM
+
+The [Azure Marketplace][Azure-Marketplace] is an online store for pre-configured applications and
+services which have been optimized for the cloud by software vendors like GitLab, and both
+the [Community Edition ("CE")][CE] and the [Enterprise Edition ("EE")][EE] versions of GitLab are
+available on the Azure Marketplace as pre-configured solutions.
+
+To begin creating a new GitLab VM, click on the **+ New** icon, type "GitLab" into the search
+box, and then click the **"GitLab Community Edition"** search result:
+
+![Azure - New - Search for 'GitLab'](img/azure-new-search-gitlab.png)
+
+A new "blade" window will pop-out, where you can read more about the **"GitLab Community Edition"**
+offering which is freely available under the MIT Expat License:
+
+![Azure - New - Select 'GitLab Community Edition'](img/azure-new-gitlab-ce.png)
+
+Click **"Create"** and you will be presented with the "Create virtual machine" blade:
+
+![Azure - Create Virtual Machine - Basics](img/azure-create-virtual-machine-basics.png)
+
+## Basics
+
+The first items we need to configure are the basic settings of the underlying virtual machine:
+
+1. Enter a `Name` for the VM - e.g. **"GitLab-CE"**
+1. Select a `VM disk type` - either **HDD** _(slower, lower cost)_ or **SSD** _(faster, higher cost)_
+1. Enter a `User name` - e.g. **"gitlab-admin"**
+1. Select an `Authentication type`, either **SSH public key** or **Password**:
+
+ >**Note:** if you're unsure which authentication type to use, select **Password**
+
+ 1. If you chose **SSH public key** - enter your `SSH public key` into the field provided
+ _(read the [SSH documentation][GitLab-Docs-SSH] to learn more about how to setup SSH
+ public keys)_
+ 1. If you chose **Password** - enter the password you wish to use _(this is the password that you
+ will use later in this tutorial to [SSH] into the VM, so make sure it's a strong password/passphrase)_
+1. Choose the appropriate `Subscription` tier for your Azure account
+1. Choose an existing `Resource Group` or create a new one - e.g. **"GitLab-CE-Azure"**
+>**Note:** a "Resource group" is a way to group related resources together for easier administration.
+We chose "GitLab-CE-Azure", but your resource group can have the same name as your VM.
+1. Choose a `Location` - if you're unsure, select the default location
+
+Here are the settings we've used:
+
+![Azure - Create Virtual Machine - Basics Completed](img/azure-create-virtual-machine-basics-password.png)
+
+Check the settings you have entered, and then click **"OK"** when you're ready to proceed.
+
+## Size
+
+Next, you need to choose the size of your VM - selecting features such as the number of CPU cores,
+the amount of RAM, the size of storage (and its speed), etc.
+
+>**Note:** in common with other cloud vendors, Azure operates a resource/usage pricing model, i.e.
+the more resources your VM consumes the more it will cost you to run, so make your selection
+carefully. You'll see that Azure provides an _estimated_ monthly cost beneath each VM Size to help
+guide your selection.
+
+The default size - the lowest cost **"DS1_V2 Standard"** VM - meets the minimum system requirements
+to run a small GitLab environment for testing and evaluation purposes, and so we're going to go
+ahead and select this one, but please choose the size which best meets your own requirements:
+
+![Azure - Create Virtual Machine - Size](img/azure-create-virtual-machine-size.png)
+
+>**Note:** be aware that whilst your VM is active (known as "allocated"), it will incur
+"compute charges" which, ultimately, you will be billed for. So, even if you're using the
+free trial credits, you'll likely want to learn
+[how to properly shutdown an Azure VM to save money][Azure-Properly-Shutdown-VM].
+
+Go ahead and click your chosen size, then click **"Select"** when you're ready to proceed to the
+next step.
+
+## Settings
+
+On the next blade, you're asked to configure the Storage, Network and Extension settings.
+We've gone with the default settings as they're sufficient for test-driving GitLab, but please
+choose the settings which best meet your own requirements:
+
+![Azure - Create Virtual Machine - Settings](img/azure-create-virtual-machine-settings.png)
+
+Review the settings and then click **"OK"** when you're ready to proceed to the last step.
+
+## Purchase
+
+The Purchase page is the last step and here you will be presented with the price per hour for your
+new VM. You'll be billed only for the VM itself (e.g. "Standard DS1 v2") because the
+**"GitLab Community Edition"** marketplace solution is free to use at 0 USD/hr:
+
+![Azure - Create Virtual Machine - Purchase](img/azure-create-virtual-machine-purchase.png)
+
+>**Note:** at this stage, you can review and modify the any of the settings you have made during all
+previous steps, just click on any of the four steps to re-open them.
+
+When you have read and agreed to the terms of use and are ready to proceed, click **"Purchase"**.
+
+## Deployment
+
+At this point, Azure will begin deploying your new VM. The deployment process will take a few
+minutes to complete, with progress displayed on the **"Deployment"** blade:
+
+![Azure - Create Virtual Machine - Deployment](img/azure-create-virtual-machine-deployment.png)
+
+Once the deployment process is complete, the new VM and its associated resources will be displayed
+on the Azure Dashboard (you may need to refresh the page):
+
+![Azure - Dashboard - All resources](img/azure-dashboard-running-resources.png)
+
+The new VM can also be accessed by clicking the `All resources` or `Virtual machines` icons in the
+Azure Portal sidebar navigation menu.
+
+## Setup a domain name
+
+The VM will have a public IP address (static by default), but Azure allows us to assign a friendly
+DNS name to the VM, so let's go ahead and do that.
+
+From the Dashboard, click on the **"GitLab-CE"** tile to open the management blade for the new VM.
+The public IP address that the VM uses is shown in the 'Essentials' section:
+
+![Azure - VM - Management - Public IP Address](img/azure-vm-management-public-ip.png)
+
+Click on the public IP address - which should open the **"Public IP address - Configuration"** blade,
+then click on **"Configuration"** (under "Settings"). Now enter a friendly DNS name for your instance
+in the `DNS name label` field:
+
+![Azure - VM - Domain Name](img/azure-vm-domain-name.png)
+
+In the screenshot above, you'll see that we've set the `DNS name label` to **"gitlab-ce-test"**.
+This will make our VM accessible at `gitlab-ce-test.centralus.cloudapp.azure.com`
+_(the full domain name of your own VM will be different, of course)_.
+
+Click **"Save"** for the changes to take effect.
+
+>**Note:** if you want to use your own domain name, you will need to add a DNS `A` record at your
+domain registrar which points to the public IP address of your Azure VM. If you do this, you'll need
+to make sure your VM is configured to use a _static_ public IP address (i.e. not a _dynamic_ one)
+or you will have to reconfigure the DNS `A` record each time Azure reassigns your VM a new public IP
+address. Read [IP address types and allocation methods in Azure][Azure-IP-Address-Types] to learn more.
+
+## Let's open some ports!
+
+At this stage you should have a running and fully operational VM. However, none of the services on
+your VM (e.g. GitLab) will be publicly accessible via the internet until you have opened up the
+neccessary ports to enable access to those services.
+
+Ports are opened by adding _security rules_ to the **"Network security group"** (NSG) which our VM
+has been assigned to. If you followed the process above, then Azure will have automatically created
+an NSG named `GitLab-CE-nsg` and assigned the `GitLab-CE` VM to it.
+
+>**Note:** if you gave your VM a different name then the NSG automatically created by Azure will
+also have a different name - the name you have your VM, with `-nsg` appended to it.
+
+You can navigate to the NSG settings via many different routes in the Azure Portal, but one of the
+simplest ways is to go to the Azure Dashboard, and then click on the Network Security Group listed
+in the **"All resources"** tile:
+
+![Azure - Dashboard - All resources - Network security group](img/azure-dashboard-highlight-nsg.png)
+
+With the **"Network security group"** blade open, click on **"Inbound security rules"** under
+**"Settings"**:
+
+![Azure - Network security group - Inbound security rules](img/azure-nsg-inbound-sec-rules-highlight.png)
+
+Next, click **"Add"**:
+
+![Azure - Network security group - Inbound security rules - Add](img/azure-nsg-inbound-sec-rules-add-highlight.png)
+
+### Which ports to open?
+
+Like all servers, our VM will be running many services. However, we want to open up the correct
+ports to enable public internet access to two services in particular:
+
+1. **HTTP** (port 80) - opening port 80 will enable our VM to respond to HTTP requests, allowing
+public access to the instance of GitLab running on our VM.
+1. **SSH** (port 22) - opening port 22 will enable our VM to respond to SSH connection requests,
+allowing public access (with authentication) to remote terminal sessions
+_(you'll see why we need [SSH] access to our VM [later on in this tutorial](#maintaining-your-gitlab-instance))_
+
+### Open HTTP on Port 80
+
+In the **"Add inbound security rule"** blade, let's open port 80 so that our VM will accept HTTP
+connections:
+
+![Azure - Add inbound security rules - HTTP](img/azure-add-inbound-sec-rule-http.png)
+
+1. Enter **"HTTP"** in the `Name` field
+1. Select **HTTP** from the options in the `Service` drop-down
+1. Make sure the `Action` is set to **Allow**
+1. Click **"OK"**
+
+### Open SSH on Port 22
+
+Repeat the above process, adding a second Inbound security rule to open port 22, enabling our VM to
+accept [SSH] connections:
+
+![Azure - Add inbound security rules - SSH](img/azure-add-inbound-sec-rule-ssh.png)
+
+1. Enter **"SSH"** in the `Name` field
+1. Select **SSH** from the options in the `Service` drop-down
+1. Make sure the `Action` is set to **Allow**
+1. Click **"OK"**
+
+
+It will take a moment for Azure to add each new Inbound Security Rule (and you may need to click on
+**"Inbound security rules"** to refresh the list), but once completed, you should see the two new
+rules in the list:
+
+![Azure - Inbound security rules - List](img/azure-inbound-sec-rules-list.png)
+
+## Connecting to GitLab
+Use the domain name you set up earlier (or the public IP address) to visit your new GitLab instance
+in your browser. If everything has gone according to plan you should be presented with the
+following page, asking you to set a _new_ password for the administrator account automatically
+created by GitLab:
+
+![GitLab - Change Password](img/gitlab-change-password.png)
+
+Enter your _new_ password into both form fields, and then click **"Change your password"**.
+
+Once you have changed the password you will be redirected to the GitLab login page. Use `root` as
+the username, enter the new password you set in the previous step, and then click **"Sign in"**:
+
+![GitLab - Login](img/gitlab-login.png)
+
+### Success?
+
+After signing in successfully, you should see the GitLab Projects page displaying a
+**"Welcome to GitLab!"** message:
+
+![GitLab - Projects Page](img/gitlab-home.png)
+
+If so, you now have a working GitLab instance on your own private Azure VM. **Congratulations!**
+
+## Creating your first GitLab project
+
+You can skip this section if you are familiar with Git and GitLab. Otherwise, let's create our first
+project. From the Welcome page, click **"New Project"**.
+
+Let's give our project a name and a description, and then accept the default values for everything
+else:
+
+1. Enter **"demo"** into the `Project path` project name field
+1. Enter a `description`, e.g. **"My awesome demo project!"**
+1. Click **"Create project"**
+
+![GitLab - New Project](img/gitlab-new-project.png)
+
+Once the new project has been created (which should only take a moment), you'll be redirected to
+homepage for the project:
+
+![GitLab - Empty Project](img/gitlab-project-home-empty.png)
+
+If you scroll further down the project's home page, you'll see some basic instructions on how to
+setup a local clone of your new repository and push and pull from it:
+
+![GitLab - Empty Project - Basic Instructions](img/gitlab-project-home-instructions.png)
+
+**That's it! You now have your own private GitLab environment installed and running in the cloud!**
+
+## Maintaining your GitLab instance
+
+It's important to keep your GitLab environment up-to-date. The GitLab team is constantly making
+enhancements and occasionally you may need to update for security reasons. So let's review how to
+update GitLab.
+
+### Checking our current version
+
+To check which version of GitLab we're currently running, click on the "Admin Area" link - it's the
+the wrench icon displayed in the top-right, next to the search box.
+
+In the following screenshot you can see an **"update asap"** notification message in the top-right.
+This particular message indicates that there is a newer version of GitLab available which contains
+one or more security fixes:
+
+![GitLab - update asap](img/gitlab-admin-area.png)
+
+Under the **"Components"** section, we can see that our VM is currently running version `8.6.5` of
+GitLab. This is the version of GitLab which was contained in the Azure Marketplace
+**"GitLab Community Edition"** offering we used to build the VM when we wrote this tutorial.
+
+>**Note:** The version of GitLab in your own VM instance may well be different, but the update
+process will still be the same.
+
+### Connect via SSH
+
+To perform an update, we need to connect directly to our Azure VM instance and run some commands
+from the terminal. Our Azure VM is actually a server running Linux (Ubuntu), so we'll need to
+connect to it using SSH ([Secure Shell][SSH]).
+
+If you're running Windows, you'll need to connect using [PuTTY] or an equivalent Windows SSH client.
+If you're running Linux or macOS, then you already have an SSH client installed.
+
+>**Note:**
+- Remember that you will need to login with the username and password you specified
+[when you created](#basics) your Azure VM
+- If you need to reset your VM password, read
+[how to reset SSH credentials for a user on an Azure VM][Azure-Troubleshoot-SSH-Connection].
+
+#### SSH from the command-line
+
+If you're running [SSH] from the command-line (terminal), then type in the following command to
+connect to your VM, substituting `username` and `your-azure-domain-name.com` for the correct values.
+
+Again, remember that your Azure VM domain name will be the one you
+[setup previously in the tutorial](#set-up-a-domain-name). If you didn't setup a domain name for
+your VM, you can use the IP address in its place in the following command:
+
+```bash
+ssh username@your-azure-domain-name.com
+```
+Provide your password at the prompt to authenticate.
+
+#### SSH from Windows (PuTTY)
+
+If you're using [PuTTY] in Windows as your [SSH] client, then you might want to take a quick
+read on [using PuTTY in Windows][Using-SSH-In-Putty].
+
+### Updating GitLab
+
+Once you've logged in via SSH, enter the following command to update GitLab to the latest
+version:
+
+```bash
+sudo apt-get update && sudo apt-get install gitlab-ce
+```
+
+This command will update GitLab and its associated components to the latest versions, so it will
+take a little time to complete. You'll see various update tasks being completed in your SSH
+terminal window:
+
+![GitLab updating](img/gitlab-ssh-update-in-progress.png)
+
+Once the update process has completed, you'll see a message like this:
+
+```
+Upgrade complete! If your GitLab server is misbehaving try running
+
+ sudo gitlab-ctl restart
+
+before anything else.
+```
+
+#### Check out your updated GitLab
+
+Refresh your GitLab instance in the browser and navigate to the Admin Area. You should now have an
+up-to-date GitLab instance.
+
+When we wrote this tutorial our Azure VM GitLab instance was updated to the latest version at time
+of writing (`9.4.0`). You can see that the message which was previously displaying **"update asap"**
+is now showing **"up-to-date"**:
+
+![GitLab up to date](img/gitlab-admin-area-9.4.0.png)
+
+## Conclusion
+
+Naturally, we believe that GitLab is a great git repository tool. However, GitLab is a whole lot
+more than that too. GitLab unifies issues, code review, CI and CD into a single UI, helping you to
+move faster from idea to production, and in this tutorial we showed you how quick and easy it is to
+setup and run your own instance of GitLab on Azure, Microsoft's cloud service.
+
+Azure is a great way to experiment with GitLab, and if you decide (as we hope) that GitLab is for
+you, you can continue to use Azure as your secure, scalable cloud provider or of course run GitLab
+on any cloud service you choose.
+
+## Where to next?
+
+Check out our other [Technical Articles][GitLab-Technical-Articles] or browse the [GitLab Documentation][GitLab-Docs] to learn more about GitLab.
+
+### Useful links
+
+- [GitLab Community Edition][CE]
+- [GitLab Enterprise Edition][EE]
+- [Microsoft Azure][Azure]
+ - [Azure - Free Account FAQ][Azure-Free-Account-FAQ]
+ - [Azure - Marketplace][Azure-Marketplace]
+ - [Azure Portal][Azure-Portal]
+ - [Azure - Pricing Calculator][Azure-Pricing-Calculator]
+ - [Azure - Troubleshoot SSH Connections to an Azure Linux VM][Azure-Troubleshoot-SSH-Connection]
+ - [Azure - Properly Shutdown an Azure VM][Azure-Properly-Shutdown-VM]
+- [SSH], [PuTTY] and [Using SSH in PuTTY][Using-SSH-In-Putty]
+
+[Original-Blog-Post]: https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/ "How to Setup a GitLab Instance on Microsoft Azure"
+[GitLab-Docs]: https://docs.gitlab.com/ce/README.html "GitLab Documentation"
+[GitLab-Technical-Articles]: https://docs.gitlab.com/ce/articles/index.html "GitLab Technical Articles"
+[GitLab-Docs-SSH]: https://docs.gitlab.com/ce/ssh/README.html "GitLab Documentation: SSH"
+[CE]: https://about.gitlab.com/features/
+[EE]: https://about.gitlab.com/features/#ee-starter
+
+[Azure-Troubleshoot-Linux-VM]: https://docs.microsoft.com/en-us/azure/virtual-machines/linux/troubleshoot-app-connection "Troubleshoot application connectivity issues on a Linux virtual machine in Azure"
+[Azure-IP-Address-Types]: https://docs.microsoft.com/en-us/azure/virtual-network/virtual-network-ip-addresses-overview-arm "IP address types and allocation methods in Azure"
+[Azure-How-To-Open-Ports]: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/nsg-quickstart-portal "How to open ports to a virtual machine with the Azure portal"
+[Azure-Troubleshoot-SSH-Connection]: https://docs.microsoft.com/en-us/azure/virtual-machines/linux/troubleshoot-ssh-connection "Troubleshoot SSH connections to an Azure Linux VM"
+[Azure]: https://azure.microsoft.com/en-us/
+[Azure-Free-Account-FAQ]: https://azure.microsoft.com/en-us/free/free-account-faq/
+[Azure-Marketplace]: https://azure.microsoft.com/en-us/marketplace/
+[Azure-Portal]: https://portal.azure.com
+[Azure-Pricing-Calculator]: https://azure.microsoft.com/en-us/pricing/calculator/
+[Azure-Properly-Shutdown-VM]: https://buildazure.com/2017/03/16/properly-shutdown-azure-vm-to-save-money/ "Properly Shutdown an Azure VM to Save Money"
+
+[SSH]: https://en.wikipedia.org/wiki/Secure_Shell
+[PuTTY]: http://www.putty.org/
+[Using-SSH-In-Putty]: https://mediatemple.net/community/products/dv/204404604/using-ssh-in-putty- \ No newline at end of file
diff --git a/doc/install/docker.md b/doc/install/docker.md
new file mode 100644
index 00000000000..933756072ff
--- /dev/null
+++ b/doc/install/docker.md
@@ -0,0 +1,18 @@
+# GitLab Docker images
+
+[Docker](https://www.docker.com) and container technology have been revolutionizing the software world for the past few years. They combine the performance and efficiency of native execution with the abstraction, security, and immutability of virtualization.
+
+GitLab provides official Docker images to allowing you to easily take advantage of the benefits of containerization while operating your GitLab instance.
+
+## Omnibus GitLab based images
+
+GitLab maintains a set of [official Docker images](https://hub.docker.com/r/gitlab) based on our [Omnibus GitLab package](https://docs.gitlab.com/omnibus/README.html). These images include:
+* [GitLab Community Edition](https://hub.docker.com/r/gitlab/gitlab-ce/)
+* [GitLab Enterprise Edition](https://hub.docker.com/r/gitlab/gitlab-ee/)
+* [GitLab Runner](https://hub.docker.com/r/gitlab/gitlab-runner/)
+
+A [complete usage guide](https://docs.gitlab.com/omnibus/docker/) to these images is available, as well as the [Dockerfile used for building the images](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker).
+
+## Cloud native images
+
+GitLab is also working towards a [cloud native set of containers](https://gitlab.com/charts/helm.gitlab.io#docker-container-images), with a single image for each component service. We intend for these images to eventually replace the [Omnibus GitLab based images](#omnibus-gitlab-based-images).
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 22aedb5403e..66eb7675896 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -66,6 +66,9 @@ Install the required packages (needed to compile Ruby and native extensions to R
sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libre2-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake
+Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but
+you can [install re2 manually](https://github.com/google/re2/wiki/Install).
+
If you want to use Kerberos for user authentication, then install libkrb5-dev:
sudo apt-get install libkrb5-dev
@@ -77,7 +80,7 @@ Make sure you have the right version of Git installed
# Install Git
sudo apt-get install -y git-core
- # Make sure Git is version 2.8.4 or higher
+ # Make sure Git is version 2.13.0 or higher
git --version
Is the system packaged Git too old? Remove it and compile from source.
@@ -296,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-4-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-5-stable gitlab
-**Note:** You can change `9-4-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `9-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -507,15 +510,17 @@ Check if GitLab and its environment are configured correctly:
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+### Compile GetText PO files
+
+ sudo -u git -H bundle exec rake gettext:pack RAILS_ENV=production
+ sudo -u git -H bundle exec rake gettext:po_to_json RAILS_ENV=production
+
### Compile Assets
sudo -u git -H yarn install --production --pure-lockfile
sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
-### Compile GetText PO files
-
- sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
-
### Start Your GitLab Instance
sudo service gitlab start
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index d2442a4fbde..a339bc23809 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -1,9 +1,9 @@
# GitLab Helm Chart
-> Officially supported cloud providers are Google Container Service and Azure Container Service.
-
-> Officially supported schedulers are Kubernetes and Terraform.
+> **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.
-The `gitlab` Helm chart deploys GitLab into your Kubernetes cluster.
+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,
This chart includes the following:
@@ -22,9 +22,7 @@ This chart includes the following:
- [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
- The `kubectl` CLI installed locally and authenticated for the cluster
-- The Helm Client installed locally
-- The Helm Server (Tiller) already installed and running in the cluster, by running `helm init`
-- The GitLab Helm Repo [added to your Helm Client](index.md#add-the-gitlab-helm-repository)
+- The [Helm client](https://github.com/kubernetes/helm/blob/master/docs/quickstart.md) installed locally on your machine
## Configuring GitLab
@@ -207,7 +205,7 @@ its class in an annotation.
>**Note:**
The Ingress alone doesn't expose GitLab externally. You need to have a Ingress controller setup to do that.
Setting up an Ingress controller can be done by installing the `nginx-ingress` helm chart. But be sure
-to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md).
+to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md).
>**Note:**
If you would like to use the Registry, you will also need to ensure your Ingress supports a [sufficiently large request size](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size).
@@ -428,6 +426,13 @@ ingress:
## Installing GitLab using the Helm Chart
> You may see a temporary error message `SchedulerPredicates failed due to PersistentVolumeClaim is not bound` while storage provisions. Once the storage provisions, the pods will automatically restart. This may take a couple minutes depending on your cloud provider. If the error persists, please review the [prerequisites](#prerequisites) to ensure you have enough RAM, CPU, and storage.
+Add the GitLab Helm repository and initialize Helm:
+
+```bash
+helm repo add gitlab https://charts.gitlab.io
+helm init
+```
+
Once you [have configured](#configuration) GitLab in your `values.yml` file,
run the following:
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
new file mode 100644
index 00000000000..d7fd8613633
--- /dev/null
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -0,0 +1,171 @@
+# GitLab-Omnibus Helm Chart
+> **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.
+
+This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work.
+
+## 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/).
+
+The deployment includes:
+
+- A [GitLab Omnibus](https://docs.gitlab.com/omnibus/) Pod, including Mattermost, Container Registry, and Prometheus
+- An auto-scaling [GitLab Runner](https://docs.gitlab.com/runner/) using the Kubernetes executor
+- [Redis](https://github.com/kubernetes/charts/tree/master/stable/redis)
+- [PostgreSQL](https://github.com/kubernetes/charts/tree/master/stable/postgresql)
+- [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/).
+
+## Prerequisites
+
+- _At least_ 4 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
+- A [wildcard DNS entry](#networking-prerequisites), which resolves to the external IP address
+- The `kubectl` CLI installed locally and authenticated for the cluster
+- The [Helm client](https://github.com/kubernetes/helm/blob/master/docs/quickstart.md) installed locally on your machine
+
+### Networking Prerequisites
+
+This chart configures a GitLab server and Kubernetes cluster which can support dynamic [Review Apps](https://docs.gitlab.com/ee/ci/review_apps/index.html), as well as services like the integrated [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html) and [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/).
+
+To support the GitLab services and dynamic environments, a wildcard DNS entry is required which resolves to the [Load Balancer](#load-balancer-ip) or [External IP](#external-ip). Configuration of the DNS entry will depend upon the DNS service being used.
+
+#### External IP (Recommended)
+
+To provision an external IP on GCP and Azure, simply request a new address from the Networking section. Ensure that the region matches the region your container cluster is created in. Note, it is important that the IP is not assigned at this point in time. It will be automatically assigned once the Helm chart is installed, and assigned to the Load Balancer.
+
+Now that an external IP address has been allocated, ensure that the wildcard DNS entry you would like to use resolves to this IP. Please consult the documentation for your DNS service for more information on creating DNS records.
+
+Finally, set the `baseIP` setting to this IP address when [deploying GitLab](#configuring-and-installing-gitlab).
+
+#### 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:
+
+`kubectl get svc -w --namespace nginx-ingress nginx`
+
+The IP address will be displayed in the `EXTERNAL-IP` field, and should be used to configure the Wildcard DNS entry. For more information on creating a wildcard DNS entry, consult the documentation for the DNS server you are using.
+
+For production deployments of GitLab, we strongly recommend using an [External IP](#external-ip).
+
+## Configuring and Installing GitLab
+
+For most installations, only two parameters are required:
+- `baseDomain`: the [base domain](#networking-prerequisites) of the wildcard host entry. For example, `mycompany.io` if the wild card entry is `*.mycompany.io`.
+- `legoEmail`: Email address to use when requesting new SSL certificates from Let's Encrypt.
+
+Other common configuration options:
+- `baseIP`: the desired [external IP address](#external-ip-recommended)
+- `gitlab`: Choose the [desired edition](https://about.gitlab.com/products), either `ee` or `ce`. `ce` is the default.
+- `gitlabEELicense`: For Enterprise Edition, the [license](https://docs.gitlab.com/ee/user/admin_area/license.html) can be installed directly via the Chart
+- `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for [Google Container Engine](https://cloud.google.com/container-engine/), with `acs` also supported for the [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/).
+
+For additional configuration options, consult the [values.yaml](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-omnibus/values.yaml).
+
+### Choosing a different GitLab release version
+
+The version of GitLab installed is based on the `gitlab` setting (see [section](#choosing-gitlab-edition) above), and
+the value of the corresponding helm setting: `gitlabCEImage` or `gitabEEImage`.
+
+```yaml
+gitlab: CE
+gitlabCEImage: gitlab/gitlab-ce:9.5.2-ce.0
+gitlabEEImage: gitlab/gitlab-ee:9.5.2-ee.0
+```
+
+The different images can be found in the [gitlab-ce](https://hub.docker.com/r/gitlab/gitlab-ce/tags/) and [gitlab-ee](https://hub.docker.com/r/gitlab/gitlab-ee/tags/)
+repositories on Docker Hub.
+
+### Persistent storage
+> **Note:**
+If you are using a machine type with support for less than 4 attached disks, like an Azure trial, you should disable dedicated storage for Postgres and Redis.
+
+By default, persistent storage is enabled for GitLab and the charts it depends
+on (Redis and PostgreSQL).
+
+Components can have their claim size set from your `values.yaml`, along with whether to provision separate storage for Postgres and Redis.
+
+Basic configuration:
+
+```yaml
+redisImage: redis:3.2.10
+redisDedicatedStorage: true
+redisStorageSize: 5Gi
+postgresImage: postgres:9.6.3
+# If you disable postgresDedicatedStorage, you should consider bumping up gitlabRailsStorageSize
+postgresDedicatedStorage: true
+postgresStorageSize: 30Gi
+gitlabRailsStorageSize: 30Gi
+gitlabRegistryStorageSize: 30Gi
+gitlabConfigStorageSize: 1Gi
+```
+
+### Routing and SSL
+
+Ingress routing and SSL are automatically configured within this Chart. An NGINX ingress is provisioned and configured, and will route traffic to any service. SSL certificates are automatically created and configured by [kube-lego](https://github.com/kubernetes/charts/tree/master/stable/kube-lego).
+
+> **Note:**
+Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [xip.io](http://xip.io) and [nip.io](http://nip.io) are unlikely to work.
+
+## Installing GitLab using the Helm Chart
+> **Note:**
+You may see a temporary error message `SchedulerPredicates failed due to PersistentVolumeClaim is not bound` while storage provisions. Once the storage provisions, the pods will automatically start. This may take a couple minutes depending on your cloud provider. If the error persists, please review the [prerequisites](#prerequisites) to ensure you have enough RAM, CPU, and storage.
+
+Add the GitLab Helm repository and initialize Helm:
+
+```bash
+helm repo add gitlab https://charts.gitlab.io
+helm init
+```
+
+Once you have reviewed the [configuration settings](#configuring-and-installing-gitlab) you can install the chart. We recommending saving your configuration options in a `values.yaml` file for easier upgrades in the future.
+
+For example:
+
+```bash
+helm install --name gitlab -f values.yaml gitlab/gitlab-omnibus
+```
+
+or passing them on the command line:
+
+```bash
+helm install --name gitlab --set baseDomain=gitlab.io,baseIP=1.1.1.1,gitlab=ee,gitlabEELicense=$LICENSE,legoEmail=email@gitlab.com gitlab/gitlab-omnibus
+```
+
+## Updating GitLab using the Helm Chart
+
+Once your GitLab Chart is installed, configuration changes and chart updates
+should we done using `helm upgrade`
+
+```bash
+helm upgrade -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab
+```
+
+where:
+
+- `<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`.
+
+## Uninstalling GitLab using the Helm Chart
+
+To uninstall the GitLab Chart, run the following:
+
+```bash
+helm delete <RELEASE-NAME>
+```
+
+where:
+
+- `<RELEASE-NAME>` is the name you gave the chart when installing it.
+ In the [Install section](#installing) we called it `gitlab`.
+
+[kube-srv]: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services---service-types
+[storageclass]: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#storageclasses
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index 515b2841d08..d31c763ed64 100644
--- a/doc/install/kubernetes/gitlab_runner_chart.md
+++ b/doc/install/kubernetes/gitlab_runner_chart.md
@@ -1,7 +1,6 @@
# GitLab Runner Helm Chart
-> Officially supported cloud providers are Google Container Service and Azure Container Service.
-
-> Officially supported schedulers are Kubernetes and Terraform.
+> **Note:**
+Officially supported cloud providers are Google Container Service and Azure Container Service.
The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your
Kubernetes cluster.
@@ -17,9 +16,7 @@ This chart configures the Runner to:
- Your GitLab Server's API is reachable from the cluster
- Kubernetes 1.4+ with Beta APIs enabled
- The `kubectl` CLI installed locally and authenticated for the cluster
-- The Helm Client installed locally
-- The Helm Server (Tiller) already installed and running in the cluster, by running `helm init`
-- The GitLab Helm Repo added to your Helm Client. See [Adding GitLab Helm Repo](index.md#add-the-gitlab-helm-repository)
+- The [Helm client](https://github.com/kubernetes/helm/blob/master/docs/quickstart.md) installed locally on your machine
## Configuring GitLab Runner using the Helm Chart
@@ -36,6 +33,8 @@ In order for GitLab Runner to function, your config file **must** specify the fo
- `runnerRegistrationToken` - The Registration Token for adding new Runners to the GitLab Server. This must be
retrieved from your GitLab Instance. See the [GitLab Runner Documentation](../../ci/runners/README.md#creating-and-registering-a-runner) for more information.
+Unless you need to specify additional configuration, you are [ready to install](#installing-gitlab-runner-using-the-helm-chart).
+
### Other configuration
The rest of the configuration is [documented in the `values.yaml`](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-runner/values.yaml) in the chart repository.
@@ -115,6 +114,17 @@ runners:
```
+### Controlling maximum Runner concurrency
+
+A single GitLab Runner deployed on Kubernetes is able to execute multiple jobs in parallel by automatically starting additional Runner pods. The [`concurrent` setting](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section) controls the maximum number of pods allowed at a single time, and defaults to `10`.
+
+```yaml
+## Configure the maximum number of concurrent jobs
+## ref: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section
+##
+concurrent: 10
+```
+
### Running Docker-in-Docker containers with GitLab Runners
See [Running Privileged Containers for the Runners](#running-privileged-containers-for-the-runners) for how to enable it,
@@ -190,6 +200,13 @@ certsSecretName: <SECRET NAME>
## Installing GitLab Runner using the Helm Chart
+Add the GitLab Helm repository and initialize Helm:
+
+```bash
+helm repo add gitlab https://charts.gitlab.io
+helm init
+```
+
Once you [have configured](#configuration) GitLab Runner in your `values.yml` file,
run the following:
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index 5ea08869a9b..fb6c0c2d263 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -1,47 +1,58 @@
# Installing GitLab on Kubernetes
> Officially supported cloud providers are Google Container Service and Azure Container Service.
-> Officially supported schedulers are Kubernetes, Terraform and Tectonic.
-
The easiest method to deploy GitLab in [Kubernetes](https://kubernetes.io/) is
-to take advantage of the official GitLab Helm charts. [Helm] is a package
+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.
-The GitLab Helm repository is located at https://charts.gitlab.io.
-You can report any issues related to GitLab's Helm Charts at
+GitLab provides [official Helm Charts](#official-gitlab-helm-charts-recommended) which are the recommended way to run GitLab within Kubernetes.
+
+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.
+
+## Official GitLab Helm Charts
+
+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.
-Contributions and improvements are also very welcome.
-## Prerequisites
+### 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.
+
+The best way to deploy GitLab on Kubernetes is to use the [gitlab-omnibus](gitlab_omnibus.md) chart.
+
+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.
+
+### Deploying just the GitLab Runner
+
+To deploy just the [GitLab Runner](https://docs.gitlab.com/runner/), utilize the [gitlab-runner](gitlab_runner_chart.md) chart.
+
+It offers a quick way to configure and deploy the Runner on Kubernetes, regardless of where your GitLab server may be running.
-To use the charts, the Helm tool must be installed and initialized. The best
-place to start is by reviewing the [Helm Quick Start Guide][helm-quick].
+### 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).
-## Add the GitLab Helm repository
+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.
-Once Helm has been installed, the GitLab chart repository must be added:
+For most deployments we recommend using our [gitlab-omnibus](gitlab_omnibus.md) chart.
-```bash
-helm repo add gitlab https://charts.gitlab.io
-```
+## Upcoming Cloud Native Helm Charts
-After adding the repository, Helm must be re-initialized:
+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).
-```bash
-helm init
-```
+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.
-## Using the GitLab Helm Charts
+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).
-GitLab makes available two Helm Charts, one for the GitLab server and another
-for the Runner. More detailed information on installing and configuring each
-Chart can be found below:
+## Community Contributed Helm Charts
-- [Install GitLab](gitlab_chart.md)
-- [Install GitLab Runner](gitlab_runner_chart.md)
+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).
[chart]: https://github.com/kubernetes/charts
-[helm-quick]: https://github.com/kubernetes/helm/blob/master/docs/quickstart.md
[helm]: https://github.com/kubernetes/helm/blob/master/README.md
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 141df55f6bc..f672b358096 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -82,7 +82,9 @@ 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.
+if your available memory changes. We also recommend [configuring the kernels 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.
@@ -104,6 +106,10 @@ features of GitLab work with MySQL/MariaDB:
See [issue #30472][30472] for more information.
1. GitLab Geo does [not support MySQL](https://docs.gitlab.com/ee/gitlab-geo/database.html#mysql-replication).
1. [Zero downtime migrations][zero] do not work with MySQL
+1. GitLab [optimizes the loading of dashboard events](https://gitlab.com/gitlab-org/gitlab-ce/issues/31806) using [PostgreSQL LATERAL JOINs](https://blog.heapanalytics.com/postgresqls-powerful-new-join-type-lateral/).
+1. In general, SQL optimized for PostgreSQL may run much slower in MySQL due to
+ differences in query planners. For example, subqueries that work well in PostgreSQL
+ may not be [performant in MySQL](https://dev.mysql.com/doc/refman/5.7/en/optimizing-subqueries.html)
1. We expect this list to grow over time.
Existing users using GitLab with MySQL/MariaDB are advised to
diff --git a/doc/integration/README.md b/doc/integration/README.md
index d70b9a7f54b..09d96bdd338 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -13,7 +13,6 @@ Bitbucket.org account
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
- [JIRA](../user/project/integrations/jira.md) Integrate with the JIRA issue tracker
-- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration
- [LDAP](ldap.md) Set up sign in via LDAP
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 6c11f46a70a..0e20b8096e9 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -224,3 +224,21 @@ By default Sign In is enabled via all the OAuth Providers that have been configu
In order to enable/disable an OmniAuth provider, go to Admin Area -> Settings -> Sign-in Restrictions section -> Enabled OAuth Sign-In sources and select the providers you want to enable or disable.
![Enabled OAuth Sign-In sources](img/enabled-oauth-sign-in-sources.png)
+
+
+## Keep OmniAuth user profiles up to date
+
+You can enable profile syncing from selected OmniAuth providers and for all or for specific user information.
+
+ ```ruby
+ gitlab_rails['sync_profile_from_provider'] = ['twitter', 'google_oauth2']
+ gitlab_rails['sync_profile_attributes'] = ['name', 'email', 'location']
+ ```
+
+ **For installations from source**
+
+ ```yaml
+ omniauth:
+ sync_profile_from_provider: ['twitter', 'google_oauth2']
+ sync_profile_claims_from_provider: ['email', 'location']
+ ``` \ No newline at end of file
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 10f5ab3370d..ae69d7f92f2 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -144,9 +144,8 @@ gitlab_rails['backup_upload_connection'] = {
'region' => 'eu-west-1',
'aws_access_key_id' => 'AKIAKIAKI',
'aws_secret_access_key' => 'secret123'
- # If using an IAM Profile, leave aws_access_key_id & aws_secret_access_key empty
- # ie. 'aws_access_key_id' => '',
- # 'use_iam_profile' => 'true'
+ # If using an IAM Profile, don't configure aws_access_key_id & aws_secret_access_key
+ # 'use_iam_profile' => true
}
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
```
diff --git a/doc/security/README.md b/doc/security/README.md
index 38706e48ec5..0fea6be8b55 100644
--- a/doc/security/README.md
+++ b/doc/security/README.md
@@ -1,6 +1,7 @@
# Security
- [Password length limits](password_length_limits.md)
+- [Restrict SSH key technologies and minimum length](ssh_keys_restrictions.md)
- [Rack attack](rack_attack.md)
- [Webhooks and insecure internal web services](webhooks.md)
- [Information exclusivity](information_exclusivity.md)
diff --git a/doc/security/img/ssh_keys_restrictions_settings.png b/doc/security/img/ssh_keys_restrictions_settings.png
new file mode 100644
index 00000000000..2e918fd4b3f
--- /dev/null
+++ b/doc/security/img/ssh_keys_restrictions_settings.png
Binary files differ
diff --git a/doc/security/ssh_keys_restrictions.md b/doc/security/ssh_keys_restrictions.md
new file mode 100644
index 00000000000..213fa5bfef5
--- /dev/null
+++ b/doc/security/ssh_keys_restrictions.md
@@ -0,0 +1,19 @@
+# Restrict allowed SSH key technologies and minimum length
+
+`ssh-keygen` allows users to create RSA keys with as few as 768 bits, which
+falls well below recommendations from certain standards groups (such as the US
+NIST). Some organizations deploying GitLab will need to enforce minimum key
+strength, either to satisfy internal security policy or for regulatory
+compliance.
+
+Similarly, certain standards groups recommend using RSA, ECDSA, or ED25519 over
+the older DSA, and administrators may need to limit the allowed SSH key
+algorithms.
+
+GitLab allows you to restrict the allowed SSH key technology as well as specify
+the minimum key length for each technology.
+
+In the Admin area under **Settings** (`/admin/application_settings`), look for
+the "Visibility and Access Controls" area:
+
+![SSH keys restriction admin settings](img/ssh_keys_restrictions_settings.png)
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index cf28f1a2eca..793de9d777c 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -193,6 +193,38 @@ How to add your SSH key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Ecl
[winputty]: https://the.earth.li/~sgtatham/putty/0.67/htmldoc/Chapter8.html#pubkey-puttygen
+## SSH on the GitLab server
+
+GitLab integrates with the system-installed SSH daemon, designating a user
+(typically named `git`) through which all access requests are handled. Users
+connecting to the GitLab server over SSH are identified by their SSH key instead
+of their username.
+
+SSH *client* operations performed on the GitLab server wil be executed as this
+user. Although it is possible to modify the SSH configuration for this user to,
+e.g., provide a private SSH key to authenticate these requests by, this practice
+is **not supported** and is strongly discouraged as it presents significant
+security risks.
+
+The GitLab check process includes a check for this condition, and will direct you
+to this section if your server is configured like this, e.g.:
+
+```
+$ gitlab-rake gitlab:check
+# ...
+Git user has default SSH configuration? ... no
+ Try fixing it:
+ mkdir ~/gitlab-check-backup-1504540051
+ sudo mv /var/lib/git/.ssh/id_rsa ~/gitlab-check-backup-1504540051
+ sudo mv /var/lib/git/.ssh/id_rsa.pub ~/gitlab-check-backup-1504540051
+ For more information see:
+ doc/ssh/README.md in section "SSH on the GitLab server"
+ Please fix the error above and rerun the checks.
+```
+
+Remove the custom configuration as soon as you're able to. These customizations
+are *explicitly not supported* and may stop working at any time.
+
## Troubleshooting
If on Git clone you are prompted for a password like `git@gitlab.com's password:`
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index 583ec5522fd..0399ebec86a 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -31,7 +31,7 @@ X-Gitlab-Event: System Hook
"path": "storecloud",
"path_with_namespace": "jsmith/storecloud",
"project_id": 74,
- "project_visibility": "private",
+ "project_visibility": "private"
}
```
@@ -48,7 +48,7 @@ X-Gitlab-Event: System Hook
"path": "underscore",
"path_with_namespace": "jsmith/underscore",
"project_id": 73,
- "project_visibility": "internal",
+ "project_visibility": "internal"
}
```
@@ -66,7 +66,7 @@ X-Gitlab-Event: System Hook
"owner_name": "John Smith",
"owner_email": "johnsmith@gmail.com",
"project_visibility": "internal",
- "old_path_with_namespace": "jsmith/overscore",
+ "old_path_with_namespace": "jsmith/overscore"
}
```
@@ -84,7 +84,7 @@ X-Gitlab-Event: System Hook
"owner_name": "John Smith",
"owner_email": "johnsmith@gmail.com",
"project_visibility": "internal",
- "old_path_with_namespace": "jsmith/overscore",
+ "old_path_with_namespace": "jsmith/overscore"
}
```
@@ -101,7 +101,7 @@ X-Gitlab-Event: System Hook
"path": "storecloud",
"path_with_namespace": "jsmith/storecloud",
"project_id": 74,
- "project_visibility": "private",
+ "project_visibility": "private"
}
```
@@ -121,7 +121,7 @@ X-Gitlab-Event: System Hook
"user_name": "John Smith",
"user_username": "johnsmith",
"user_id": 41,
- "project_visibility": "private",
+ "project_visibility": "private"
}
```
@@ -141,7 +141,7 @@ X-Gitlab-Event: System Hook
"user_name": "John Smith",
"user_username": "johnsmith",
"user_id": 41,
- "project_visibility": "private",
+ "project_visibility": "private"
}
```
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 0c0d482499a..fac91935a45 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -37,7 +37,6 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [Private Tokens](../../api/README.md#private-tokens)
- [Impersonation tokens](../../api/README.md#impersonation-tokens)
- [GitLab as an OAuth2 provider](../../api/oauth2.md#gitlab-as-an-oauth2-provider)
-- [GitLab Runner API - Authentication](../../api/ci/runners.md#authentication)
## Third-party resources
diff --git a/doc/topics/autodevops/img/auto_devops_settings.png b/doc/topics/autodevops/img/auto_devops_settings.png
new file mode 100644
index 00000000000..57bd7650a30
--- /dev/null
+++ b/doc/topics/autodevops/img/auto_devops_settings.png
Binary files differ
diff --git a/doc/topics/autodevops/img/auto_monitoring.png b/doc/topics/autodevops/img/auto_monitoring.png
new file mode 100644
index 00000000000..5661b50841b
--- /dev/null
+++ b/doc/topics/autodevops/img/auto_monitoring.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_connect_cluster.png b/doc/topics/autodevops/img/guide_connect_cluster.png
new file mode 100644
index 00000000000..b856b81a1d0
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_connect_cluster.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_integration.png b/doc/topics/autodevops/img/guide_integration.png
new file mode 100644
index 00000000000..723b2619ea2
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_integration.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_secret.png b/doc/topics/autodevops/img/guide_secret.png
new file mode 100644
index 00000000000..01f5aa49908
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_secret.png
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
new file mode 100644
index 00000000000..babf44d2665
--- /dev/null
+++ b/doc/topics/autodevops/index.md
@@ -0,0 +1,342 @@
+# 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)
+
+## 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.
+
+## 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.
+
+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.
+
+## Prerequisites
+
+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.
+
+If you are using GitLab outside of Kubernetes, for example with GitLab.com, then
+you should take these prerequisites into account:
+
+1. **Base domain** - You will need a base domain configured with wildcard DNS to
+ be used by all of your Auto DevOps applications.
+
+1. **GitLab Runner** - 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).
+ The Runners do not need to be installed in the Kubernetes cluster, but the
+ Kubernetes executor is easy to use and is automatically autoscaling.
+ Docker-based Runners can be configured to autoscale as well, using [Docker
+ Machine](https://docs.gitlab.com/runner/install/autoscaling.html). Runners
+ 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]
+ 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).
+ 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
+
+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.
+
+![auto devops settings](img/auto_devops_settings.png)
+
+## Stages of Auto DevOps
+
+The following sections describe the stages of Auto DevOps.
+
+### Auto Build
+
+Auto Build creates a build of the application in one of two ways:
+
+- If there is a `Dockerfile`, it will use `docker build` to create a Docker image.
+- Otherwise, it will use [Herokuish](https://github.com/gliderlabs/herokuish)
+ and [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks)
+ 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.
+
+### Auto Test
+
+Auto Test automatically tests 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).
+
+Auto Test uses tests you already have in your application. If there are no
+tests, it's up to you to add them.
+
+### Auto Code Quality
+
+Auto Code Quality 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.*
+
+### 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
+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`.
+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 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
+enable them.
+
+### Auto Monitoring
+
+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).
+
+* Response Metrics: latency, throughput, error rate
+* System Metrics: CPU utilization, memory utilization
+
+To view the metrics, open the [Monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments).
+
+![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
+configuration is required.
+
+If you have installed GitLab using a different method:
+
+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
+
+### PostgreSQL Database Support
+
+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
+
+Any variables set at the project or group level will override variables set in
+the CI/CD configuration.
+
+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
+
+### Custom buildpack
+
+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
+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
+
+ >**Note:** Using multiple buildpacks may break Auto Test.
+
+### Custom `Dockerfile`
+
+If your project has a `Dockerfile` in the root of the project repo, Auto DevOps
+will build a Docker image based on the Dockerfile rather than using buildpacks.
+This can be much faster and result in smaller images, especially if your
+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.
+
+**Project variable** - Create a project variable `AUTO_DEVOPS_CHART` with the
+URL of a custom chart to use.
+
+### Enable staging, canaries, and more with custom `.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.
+
+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.
+
+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
+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.
+
+## Currently supported languages
+
+>**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)
+support it, and some third-party buildpacks as well e.g., Go, Node, Java, PHP,
+Python, Ruby, Gradle, Scala, and Elixir all support Auto Test, but notably the
+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
+```
+
+## Private Project Support - Experimental
+
+When a project has been marked as private, GitLab's [Container
+Registry][container-registry] requires authentication when downloading
+containers. Auto DevOps will automatically provide the required authentication
+information to Kubernetes, allowing temporary access to the registry.
+Authentication credentials will be valid while the pipeline is running, allowing
+for a successful initial deployment.
+
+After the pipeline completes, Kubernetes will no longer be able to access the
+container registry. **Restarting a pod, scaling a service, or other actions which
+require on-going access to the registry will fail**. On-going secure access is
+planned for a subsequent release.
+
+## Troubleshooting
+
+- Auto Build and Auto Test may fail in detecting your language/framework. There
+ may be no buildpack for your application, or your application may be missing the
+ 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).
+- 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.
+
+[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/
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
new file mode 100644
index 00000000000..f23c9d794b4
--- /dev/null
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -0,0 +1,95 @@
+# Auto DevOps: quick start guide
+
+> [Introduced][ce-37115] in GitLab 10.0. Auto DevOps is currently in Beta and
+**not recommended for production use**.
+
+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:
+
+* `server.rb` - our application. It will start an HTTP server on port 5000 and
+ render "Hello, world!"
+* `Dockerfile` - to build our app into a container image. It will use a ruby
+ base image and run `server.rb`
+
+## 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.
+
+## 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.
+
+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):
+
+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`
+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`.
+
+![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.
+
+![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.
+
+![connect to cluster](img/guide_integration.png)
+
+For 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.
+
+### Set up Ingress controller
+
+You’ll need to make sure you have an ingress controller. If you don’t have one, do:
+
+```sh
+brew install kubernetes-helm
+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.
+
+### Point DNS at Cluster IP
+
+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 `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.
+
+![auto devops settings](img/auto_devops_settings.png)
+
+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 its ready - just visit http://minimal-ruby-app.example.com to see “Hello, world!â€
+
+[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md
index 604f9375714..df56f031970 100644
--- a/doc/topics/git/index.md
+++ b/doc/topics/git/index.md
@@ -22,6 +22,7 @@ We've gathered some resources to help you to get the best from Git with GitLab.
- [Cherry-picking a commit](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit)
- [Squashing commits](../../workflow/gitlab_flow.md#squashing-commits-with-rebase)
- **Articles:**
+ - [Numerous _undo_ possibilities in Git](../../articles/numerous_undo_possibilities_in_git/index.md)
- [How to install Git](../../articles/how_to_install_git/index.md)
- [Git Tips & Tricks](https://about.gitlab.com/2016/12/08/git-tips-and-tricks/)
- [Eight Tips to help you work better with Git](https://about.gitlab.com/2015/02/19/8-tips-to-help-you-work-better-with-git/)
diff --git a/doc/topics/index.md b/doc/topics/index.md
index ad388dff822..b51f24b02e4 100644
--- a/doc/topics/index.md
+++ b/doc/topics/index.md
@@ -7,6 +7,7 @@ you through better understanding GitLab's concepts
through our regular docs, and, when available, through articles (guides,
tutorials, technical overviews, blog posts) and videos.
+- [Auto DevOps](autodevops/index.md)
- [Authentication](authentication/index.md)
- [Continuous Integration (GitLab CI)](../ci/README.md)
- [Git](git/index.md)
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
index 4d3ababaa41..2abc57da1a0 100644
--- a/doc/update/8.17-to-9.0.md
+++ b/doc/update/8.17-to-9.0.md
@@ -264,6 +264,16 @@ sudo systemctl daemon-reload
### 9. Install libs, migrations, etc.
+GitLab 9.0.11 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/24570)
+a dependency on on the `re2` regular expression library. To install this dependency:
+
+```bash
+sudo apt-get install libre2-dev
+```
+
+Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but
+you can [install re2 manually](https://github.com/google/re2/wiki/Install).
+
```bash
cd /home/git/gitlab
diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md
index 2b4a7bed27f..3fd1d023d2a 100644
--- a/doc/update/9.0-to-9.1.md
+++ b/doc/update/9.0-to-9.1.md
@@ -264,6 +264,16 @@ sudo systemctl daemon-reload
### 9. Install libs, migrations, etc.
+GitLab 9.1.8 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/24570)
+a dependency on on the `re2` regular expression library. To install this dependency:
+
+```bash
+sudo apt-get install libre2-dev
+```
+
+Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but
+you can [install re2 manually](https://github.com/google/re2/wiki/Install).
+
```bash
cd /home/git/gitlab
diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md
index f38547bba1a..5f7a616cc7d 100644
--- a/doc/update/9.1-to-9.2.md
+++ b/doc/update/9.1-to-9.2.md
@@ -100,6 +100,7 @@ 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:
@@ -221,6 +222,16 @@ sudo systemctl daemon-reload
### 10. Install libs, migrations, etc.
+GitLab 9.2.8 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/24570)
+a dependency on on the `re2` regular expression library. To install this dependency:
+
+```bash
+sudo apt-get install libre2-dev
+```
+
+Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but
+you can [install re2 manually](https://github.com/google/re2/wiki/Install).
+
```bash
cd /home/git/gitlab
@@ -236,6 +247,11 @@ 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:pack RAILS_ENV=production
+sudo -u git -H bundle exec rake gettext:po_to_json 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
diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md
index 373f43eb3bb..9d0b0da7edb 100644
--- a/doc/update/9.2-to-9.3.md
+++ b/doc/update/9.2-to-9.3.md
@@ -100,6 +100,7 @@ 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:
@@ -257,6 +258,16 @@ sudo systemctl daemon-reload
### 12. Install libs, migrations, etc.
+GitLab 9.3.8 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/24570)
+a dependency on on the `re2` regular expression library. To install this dependency:
+
+```bash
+sudo apt-get install libre2-dev
+```
+
+Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but
+you can [install re2 manually](https://github.com/google/re2/wiki/Install).
+
```bash
cd /home/git/gitlab
@@ -272,6 +283,10 @@ 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
diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md
index b167f0737aa..9ee01bc9c51 100644
--- a/doc/update/9.3-to-9.4.md
+++ b/doc/update/9.3-to-9.4.md
@@ -100,6 +100,7 @@ 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:
@@ -270,6 +271,16 @@ sudo systemctl daemon-reload
### 12. Install libs, migrations, etc.
+GitLab 9.4 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/24570)
+a dependency on on the `re2` regular expression library. To install this dependency:
+
+```bash
+sudo apt-get install libre2-dev
+```
+
+Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but
+you can [install re2 manually](https://github.com/google/re2/wiki/Install).
+
```bash
cd /home/git/gitlab
@@ -285,6 +296,10 @@ 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
diff --git a/doc/update/9.4-to-9.5.md b/doc/update/9.4-to-9.5.md
index fc87b2d0f1e..1b5a15589af 100644
--- a/doc/update/9.4-to-9.5.md
+++ b/doc/update/9.4-to-9.5.md
@@ -295,6 +295,10 @@ 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
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index ac1bcb8f241..30107360446 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -35,7 +35,7 @@ current version with `cat VERSION`).
cd /home/git/gitlab
sudo -u git -H git fetch --all
-sudo -u git -H git checkout -- Gemfile.lock db/schema.rb
+sudo -u git -H git checkout -- Gemfile.lock db/schema.rb locale
sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG
```
@@ -56,6 +56,12 @@ 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
+# Internationalization was added in `v9.2.0` so these commands are only
+# required for versions equal or major to it.
+sudo -u git -H bundle exec rake gettext:pack RAILS_ENV=production
+sudo -u git -H bundle exec rake gettext:po_to_json RAILS_ENV=production
+
# Clean up assets and cache
sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production NODE_ENV=production
```
diff --git a/doc/user/discussions/img/automatically_resolve_outdated_discussions.png b/doc/user/discussions/img/automatically_resolve_outdated_discussions.png
new file mode 100644
index 00000000000..9a798ddd178
--- /dev/null
+++ b/doc/user/discussions/img/automatically_resolve_outdated_discussions.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 8b1d299484c..efea99eb120 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -116,6 +116,23 @@ are resolved.
![Only allow merge if all the discussions are resolved message](img/only_allow_merge_if_all_discussions_are_resolved_msg.png)
+### Automatically resolve merge request diff discussions when they become outdated
+
+> [Introduced][ce-14053] in GitLab 10.0.
+
+You can automatically resolve merge request diff discussions on lines modified
+with a new push.
+
+Navigate to your project's settings page, select the **Automatically resolve
+merge request diffs discussions on lines changed with a push** check box and hit
+**Save** for the changes to take effect.
+
+![Automatically resolve merge request diff discussions when they become outdated](img/automatically_resolve_outdated_discussions.png)
+
+From now on, any discussions on a diff will be resolved by default if a push
+makes that diff section outdated. Discussions on lines that don't change and
+top-level resolvable discussions are not automatically resolved.
+
## Threaded discussions
> [Introduced][ce-7527] in GitLab 9.1.
@@ -141,6 +158,7 @@ comments in greater detail.
[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
[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/img/share_with_group_lock.png b/doc/user/group/img/share_with_group_lock.png
index 8df41bf9465..c0f25389eaf 100644
--- a/doc/user/group/img/share_with_group_lock.png
+++ b/doc/user/group/img/share_with_group_lock.png
Binary files differ
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index 08da721c71d..db0242f1324 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -55,8 +55,18 @@ By doing so:
- John mentions everyone from his team with `@john-team`
- John mentions only his marketing team with `@john-team/marketing`
+## Issues and merge requests within a group
+
+Issues and merge requests are part of projects. For a given group, view all the
+[issues](../project/issues/index.md#issues-per-group) and [merge requests](../project/merge_requests/index.md#merge-requests-per-group) across all the projects in that group,
+together in a single list view.
+
## Create a new group
+> **Notes:**
+- For a list of words that are not allowed to be used as group names see the
+ [reserved names][reserved].
+
You can create a group in GitLab from:
1. The Groups page: expand the left menu, click **Groups**, and click the green button **New group**:
@@ -153,6 +163,13 @@ Find this option under your project's settings.
GitLab administrators can use the admin interface to move any project to any namespace if needed.
+## Sharing a project with a group
+
+You can [share your projects with a group](../project/members/share_project_with_groups.md)
+and give your group members access to the project all at once.
+
+Alternatively, you can [lock the sharing with group feature](#share-with-group-lock).
+
## Manage group memberships via LDAP
In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups.
@@ -173,10 +190,28 @@ access further configurations for your group.
#### Enforce 2FA to group members
-Add a secury layer to your group by
+Add a security layer to your group by
[enforcing two-factor authentication (2FA)](../../security/two_factor_authentication.md#enforcing-2fa-for-all-users-in-a-group)
to all group members.
+#### Share with group lock
+
+Prevent projects in a group from [sharing
+a project with another group](../project/members/share_project_with_groups.md).
+This allows for tighter control over project access.
+
+For example, consider you have two distinct teams (Group A and Group B)
+working together in a project.
+To inherit the group membership, you share the project between the
+two groups A and B. **Share with group lock** prevents any project within
+the group from being shared with another group. By doing so, you
+guarantee only the right group members have access to that projects.
+
+To enable this feature, navigate to the group settings page. Select
+**Share with group lock** and **Save the group**.
+
+![Checkbox for share with group lock](img/share_with_group_lock.png)
+
#### Member Lock (EES/EEP)
Available in [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/),
@@ -185,15 +220,6 @@ level of members in group.
Learn more about [Member Lock](https://docs.gitlab.com/ee/user/group/index.html#member-lock-ees-eep).
-#### Share with group lock (EES/EEP)
-
-In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)
-it is possible to prevent projects in a group from [sharing
-a project with another group](../../workflow/share_projects_with_other_groups.md).
-This allows for tighter control over project access.
-
-Learn more about [Share with group lock](https://docs.gitlab.com/ee/user/group/index.html#share-with-group-lock-ees-eep).
-
### Advanced settings
- **Projects**: view all projects within that group, add members to each project,
@@ -205,4 +231,5 @@ for the group (GitLab admins only, available in [GitLab Enterprise Edition Start
- **Pipelines quota**: keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group
[permissions]: ../permissions.md#permissions
-[ee]: https://about.gitlab.com/products/ \ No newline at end of file
+[ee]: https://about.gitlab.com/products/
+[reserved]: ../reserved_names.md
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index 5724dcfab48..d2478aea4bd 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -83,10 +83,7 @@ structure.
- You need to be an Owner of a group in order to be able to create
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
- [`path_regex.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `PROJECT_WILDCARD_ROUTES` and `GROUP_ROUTES` lists:
- - `TOP_LEVEL_ROUTES`: are names that are reserved as usernames or top level groups
- - `PROJECT_WILDCARD_ROUTES`: are names that are reserved for child groups or projects.
- - `GROUP_ROUTES`: are names that are reserved for all groups or projects.
+ [reserved names][reserved].
To create a subgroup:
@@ -175,5 +172,5 @@ Here's a list of what you can't do with subgroups:
[ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772
[permissions]: ../../permissions.md#group
-[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/path_regex.rb
+[reserved]: ../../reserved_names.md
[issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30472#note_27747600
diff --git a/doc/user/index.md b/doc/user/index.md
index 1281cc6e4f0..f239a15d441 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -71,80 +71,44 @@ your code, use it as an issue tracker, collaborate on code, and continuously
build, test, and deploy your app with built-in GitLab CI/CD. Or, you can do
it all at once, from one single project.
-### Repository
-
-Host your codebase in [GitLab repositories](project/repository/index.md) with version control
-and as part of a fully integrated platform.
-
-### Issues
-
-Explore the best of GitLab [Issues](project/issues/index.md).
-
-### Merge Requests
-
-Collanorate on code, gather reviews, live preview changes per branch, and
-request approvals with [Merge Requests](project/merge_requests/index.md).
-
-### Milestones
-
-Work on multiple issues and merge requests towards the same target date
-with [Milestones](project/milestones/index.md).
-
-### GitLab Pages
-
-Publish your static site directly from GitLab with [GitLab Pages](project/pages/index.md). You
-can [build, test, and deploy any Static Site Generator](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) with Pages.
-
-### Container Registry
-
-Build and deploy Docker images with [GitLab Container Registry](project/container_registry.md).
+- [Repositories](project/repository/index.md): Host your codebase in
+repositories with version control and as part of a fully integrated platform.
+- [Issues](project/issues/index.md): Explore the best of GitLab Issues' features.
+- [Merge Requests](project/merge_requests/index.md): Collaborate on code,
+reviews, live preview changes per branch, and request approvals with Merge Requests.
+- [Milestones](project/milestones/index.md): Work on multiple issues and merge
+requests towards the same target date with Milestones.
## GitLab CI/CD
Use built-in [GitLab CI/CD](../ci/README.md) to test, build, and deploy your applications
directly from GitLab. No third-party integrations needed.
-### Auto Deploy
-
-Deploy your application out-of-the-box with [GitLab Auto Deploy](../ci/autodeploy/index.md).
-
-### Review Apps
-
-Live-preview the changes introduced by a merge request with [Review Apps](../ci/review_apps/index.md).
-
-## Groups
-
-With GitLab [Groups](group/index.md) you can assemble related projects together
-and grant members access to several projects at once.
-
-### Subgroups
-
-Groups can also be nested in [subgroups](group/subgroups/index.md).
+- [GitLab Auto Deploy](../ci/autodeploy/index.md): Deploy your application out-of-the-box with GitLab Auto Deploy.
+- [Review Apps](../ci/review_apps/index.md): Live-preview the changes introduced by a merge request with Review Apps.
+- [GitLab Pages](project/pages/index.md): Publish your static site directly from
+GitLab with Gitlab Pages. You can build, test, and deploy any Static Site Generator with Pages.
+- [GitLab Container Registry](project/container_registry.md): Build and deploy Docker
+images with Container Registry.
## Account
There is a lot you can customize and configure
to enjoy the best of GitLab.
-[Manage your user settings](profile/index.md) to change your personal info,
+- [Settings](profile/index.md): Manage your user settings to change your personal info,
personal access tokens, authorized applications, etc.
+- [Authentication](../topics/authentication/index.md): Read through the authentication
+methods available in GitLab.
+- [Permissions](permissions.md): Learn the different set of permissions levels for each
+user type (guest, reporter, developer, master, owner).
-### Authentication
-
-Read through the [authentication](../topics/authentication/index.md) methods available in GitLab.
-
-### Permissions
-
-Learn the different set of [permissions](permissions.md) for user type (guest, reporter, developer, master, owner).
-
-## Integrations
-
-[Integrate GitLab](../integration/README.md) with your preferred tool,
-such as Trello, JIRA, etc.
+## Groups
-## Git and GitLab
+With GitLab [Groups](group/index.md) you can assemble related projects together
+and grant members access to several projects at once.
-Learn what is [Git](../topics/git/index.md) and its best practices.
+Groups can also be nested in [subgroups](group/subgroups/index.md).
## Discussions
@@ -155,6 +119,13 @@ When performing inline reviews to implementations
to your codebase through merge requests you can
gather feedback through [resolvable discussions](discussions/index.md#resolvable-discussions).
+### GitLab Flavored Markdown (GFM)
+
+Read through the [GFM documentation](markdown.md) to learn how to apply
+the best of GitLab Flavored Markdown in your discussions, comments,
+issues and merge requests descriptions, and everywhere else GMF is
+supported.
+
## Todos
Never forget to reply to your collaborators. [GitLab Todos](../workflow/todos.md)
@@ -162,12 +133,21 @@ are a tool for working faster and more effectively with your team,
by listing all user or group mentions, as well as issues and merge
requests you're assigned to.
+## Search
+
+[Search and filter](search/index.md) through groups, projects, issues, merge requests, files, code, and more.
+
## Snippets
[Snippets](snippets.md) are code blocks that you want to store in GitLab, from which
you have quick access to. You can also gather feedback on them through
[discussions](#discussions).
+## Integrations
+
+[Integrate GitLab](../integration/README.md) with your preferred tool,
+such as Trello, JIRA, etc.
+
## Webhooks
Configure [webhooks](project/integrations/webhooks.html) to listen for
@@ -178,3 +158,6 @@ POST request with data to the webhook URL.
Automate GitLab via [API](../api/README.html).
+## Git and GitLab
+
+Learn what is [Git](../topics/git/index.md) and its best practices.
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 3d47e644ad2..0c17905aa8c 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -5,32 +5,32 @@ particular group or project. If a user is both in a group's project and the
project itself, the highest permission level is used.
On public and internal projects the Guest role is not enforced. All users will
-be able to create issues, leave comments, and pull or download the project code.
+be able to create issues, leave comments, and clone or download the project code.
-When a member leaves the team the all assigned Issues and Merge Requests
+When a member leaves the team all the assigned [Issues](project/issues/index.md) and [Merge Requests](project/merge_requests/index.md)
will be unassigned automatically.
-GitLab administrators receive all permissions.
+GitLab [administrators](../README.md#administrator-documentation) receive all permissions.
-To add or import a user, you can follow the [project users and members
-documentation](../workflow/add-user/add-user.md).
+To add or import a user, you can follow the
+[project members documentation](../user/project/members/index.md).
-## Project
+## Project members permissions
The following table depicts the various user permission levels in a project.
| Action | Guest | Reporter | Developer | Master | Owner |
|---------------------------------------|---------|------------|-------------|----------|--------|
-| Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ |
-| Create confidential issue | ✓ | ✓ | ✓ | ✓ | ✓ |
-| View confidential issues | (✓) [^1] | ✓ | ✓ | ✓ | ✓ |
-| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ |
-| See a list of jobs | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
-| See a job log | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
-| Download and browse job artifacts | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
-| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ |
-| Pull project code | | ✓ | ✓ | ✓ | ✓ |
-| Download project | | ✓ | ✓ | ✓ | ✓ |
+| Create new issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| Create confidential issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| View confidential issues | (✓) [^2] | ✓ | ✓ | ✓ | ✓ |
+| Leave comments | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| See a list of jobs | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
+| See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
+| Download and browse job artifacts | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
+| View wiki pages | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| Pull project code | [^1] | ✓ | ✓ | ✓ | ✓ |
+| Download project | [^1] | ✓ | ✓ | ✓ | ✓ |
| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ |
| Manage labels | | ✓ | ✓ | ✓ | ✓ |
@@ -71,11 +71,62 @@ The following table depicts the various user permission levels in a project.
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
-| Force push to protected branches [^3] | | | | | |
-| Remove protected branches [^3] | | | | | |
+| Force push to protected branches [^4] | | | | | |
+| Remove protected branches [^4] | | | | | |
| Remove pages | | | | | ✓ |
-## Group
+## Project features permissions
+
+### Wiki and issues
+
+Project features like wiki and issues can be hidden from users depending on
+which visibility level you select on project settings.
+
+- Disabled: disabled for everyone
+- Only team members: only team members will see even if your project is public or internal
+- Everyone with access: everyone can see depending on your project visibility level
+
+### Protected branches
+
+To prevent people from messing with history or pushing code without
+review, we've created protected branches. Read through the documentation on
+[protected branches](project/protected_branches.md)
+to learn more.
+
+Additionally, you can allow or forbid users with Master and/or
+Developer permissions to push to a protected branch. Read through the documentation on
+[Allowed to Merge and Allowed to Push settings](project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings)
+to learn more.
+
+### Cycle Analytics permissions
+
+Find the current permissions on the Cycle Analytics dashboard on
+the [documentation on Cycle Analytics permissions](project/cycle_analytics.md#permissions).
+
+### Issue Board permissions
+
+Developers and users with higher permission level can use all
+the functionality of the Issue Board, that is create/delete lists
+and drag issues around. Read though the
+[documentation on Issue Boards permissions](project/issue_board.md#permissions)
+to learn more.
+
+### File Locking permissions (EEP)
+
+The user that locks a file or directory is the only one that can edit and push their changes back to the repository where the locked objects are located.
+
+Read through the documentation on [permissions for File Locking](https://docs.gitlab.com/ee/user/project/file_lock.html#permissions-on-file-locking) to learn more.
+
+File Locking is available in
+[GitLab Enterprise Edition Premium](https://about.gitlab.com/gitlab-ee/) only.
+
+### Confidential Issues permissions
+
+Confidential issues can be accessed by reporters and higher permission levels,
+as well as by guest users that create a confidential issue. To learn more,
+read through the documentation on [permissions and access to confidential issues](project/issues/confidential_issues.md#permissions-and-access-to-confidential-issues).
+
+## Group members permissions
Any user can remove themselves from a group, unless they are the last Owner of
the group. The following table depicts the various user permission levels in a
@@ -91,7 +142,16 @@ group.
| Remove group | | | | | ✓ |
| Manage group labels | | ✓ | ✓ | ✓ | ✓ |
-## External Users
+### Subgroup permissions
+
+When you add a member to a subgroup, they inherit the membership and
+permission level from the parent group. This model allows access to
+nested groups if you have membership in one of its parents.
+
+To learn more, read through the documentation on
+[subgroups memberships](group/subgroups/index.md#membership).
+
+## External users permissions
In cases where it is desired that a user has access only to some internal or
private projects, there is the option of creating **External Users**. This
@@ -115,18 +175,9 @@ will find the option to flag the user as external.
By default new users are not set as external users. This behavior can be changed
by an administrator under **Admin > Application Settings**.
-## Project features
-
-Project features like wiki and issues can be hidden from users depending on
-which visibility level you select on project settings.
-
-- Disabled: disabled for everyone
-- Only team members: only team members will see even if your project is public or internal
-- Everyone with access: everyone can see depending on your project visibility level
-
-## GitLab CI
+## GitLab CI/CD permissions
-GitLab CI permissions rely on the role the user has in GitLab. There are four
+GitLab CI/CD permissions rely on the role the user has in GitLab. There are four
permission levels in total:
- admin
@@ -134,7 +185,7 @@ permission levels in total:
- developer
- guest/reporter
-The admin user can perform any action on GitLab CI in scope of the GitLab
+The admin user can perform any action on GitLab CI/CD in scope of the GitLab
instance and project. In addition, all admins can use the admin interface under
`/admin/runners`.
@@ -150,7 +201,7 @@ instance and project. In addition, all admins can use the admin interface under
| See events in the system | | | | ✓ |
| Admin interface | | | | ✓ |
-### Jobs permissions
+### Job permissions
>**Note:**
GitLab 8.12 has a completely redesigned job permissions system.
@@ -164,20 +215,47 @@ users:
| Run CI job | | ✓ | ✓ | ✓ |
| Clone source and LFS from current project | | ✓ | ✓ | ✓ |
| Clone source and LFS from public projects | | ✓ | ✓ | ✓ |
-| Clone source and LFS from internal projects | | ✓ [^4] | ✓ [^4] | ✓ |
-| Clone source and LFS from private projects | | ✓ [^5] | ✓ [^5] | ✓ [^5] |
+| Clone source and LFS from internal projects | | ✓ [^5] | ✓ [^5] | ✓ |
+| Clone source and LFS from private projects | | ✓ [^6] | ✓ [^6] | ✓ [^6] |
| Push source and LFS | | | | |
| Pull container images from current project | | ✓ | ✓ | ✓ |
| Pull container images from public projects | | ✓ | ✓ | ✓ |
-| Pull container images from internal projects| | ✓ [^4] | ✓ [^4] | ✓ |
-| Pull container images from private projects | | ✓ [^5] | ✓ [^5] | ✓ [^5] |
+| Pull container images from internal projects| | ✓ [^5] | ✓ [^5] | ✓ |
+| Pull container images from private projects | | ✓ [^6] | ✓ [^6] | ✓ [^6] |
| Push container images to current project | | ✓ | ✓ | ✓ |
| Push container images to other projects | | | | |
-[^1]: Guest users can only view the confidential issues they created themselves
-[^2]: If **Public pipelines** is enabled in **Project Settings > Pipelines**
-[^3]: Not allowed for Guest, Reporter, Developer, Master, or Owner
-[^4]: Only if user is not external one.
-[^5]: Only if user is a member of the project.
+### New CI job permissions model
+
+GitLab 8.12 has a completely redesigned job permissions system. To learn more,
+read through the documentation on the [new CI/CD permissions model](project/new_ci_build_permissions_model.md#new-ci-job-permissions-model).
+
+## Running pipelines on protected branches
+
+The permission to merge or push to protected branches is used to define if a user can
+run CI/CD pipelines and execute actions on jobs that are related to those branches.
+
+See [Security on protected branches](../ci/pipelines.md#security-on-protected-branches)
+for details about the pipelines security model.
+
+## LDAP users permissions
+
+Since GitLab 8.15, LDAP user permissions can now be manually overridden by an admin user.
+Read through the documentation on [LDAP users permissions](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/index.html#updating-user-permissions-new-feature) to learn more.
+
+## Auditor users permissions (EEP)
+
+An Auditor user should be able to access all projects and groups of a GitLab instance
+with the permissions described on the documentation on [auditor users permissions](https://docs.gitlab.com/ee/administration/auditor_users.html#permissions-and-restrictions-of-an-auditor-user).
+
+Auditor users are available in [GitLab Enterprise Edition Premium](https://about.gitlab.com/gitlab-ee/)
+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**
+[^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.
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md
diff --git a/doc/user/profile/img/profile_settings_dropdown.png b/doc/user/profile/img/profile_settings_dropdown.png
new file mode 100644
index 00000000000..a2c620642e2
--- /dev/null
+++ b/doc/user/profile/img/profile_settings_dropdown.png
Binary files differ
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 629d69d8aea..5c615daf464 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -120,6 +120,11 @@ If a project is private, credentials will need to be provided for authorization.
The preferred way to do this, is by using [personal access tokens][pat].
The minimal scope needed is `read_registry`.
+Example of using a personal access token:
+```
+docker login registry.example.com -u <your_username> -p <your_personal_access_token>
+```
+
## Troubleshooting the GitLab Container Registry
### Basic Troubleshooting
diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md
index ea7496af089..7c94f4ef4d8 100644
--- a/doc/user/project/description_templates.md
+++ b/doc/user/project/description_templates.md
@@ -39,4 +39,58 @@ changes you made after picking the template and return it to its initial status.
![Description templates](img/description_templates.png)
+## Description template example
+
+We make use of Description Templates for Issues and Merge Requests within the GitLab Community Edition project. Please refer to the [`.gitlab` folder][gitlab-ce-templates] for some examples.
+
+> **Tip:**
+It is possible to use [quick actions](./quick_actions.md) within description templates to quickly add labels, assignees, and milestones. The quick actions will only be executed if the user submitting the Issue or Merge Request has the permissions perform the relevant actions.
+
+Here is an example for a Bug report template:
+
+```
+Summary
+
+(Summarize the bug encountered concisely)
+
+
+Steps to reproduce
+
+(How one can reproduce the issue - this is very important)
+
+
+Example Project
+
+(If possible, please create an example project here on GitLab.com that exhibits the problematic behaviour, and link to it here in the bug report)
+
+(If you are using an older version of GitLab, this will also determine whether the bug has been fixed in a more recent version)
+
+
+What is the current bug behavior?
+
+(What actually happens)
+
+
+What is the expected correct behavior?
+
+(What you should see instead)
+
+
+Relevant logs and/or screenshots
+
+(Paste any relevant logs - please use code blocks (```) to format console output,
+logs, and code as it's very hard to read otherwise.)
+
+
+Possible fixes
+
+(If you can, link to the line of code that might be responsible for the problem)
+
+/label ~bug ~reproduced ~needs-investigation
+/cc @project-manager
+/assign @qa-tester
+```
+
[ce-4981]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4981
+[gitlab-ce-templates]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/.gitlab
+
diff --git a/doc/user/project/gpg_signed_commits/index.md b/doc/user/project/gpg_signed_commits/index.md
new file mode 100644
index 00000000000..261eedeb412
--- /dev/null
+++ b/doc/user/project/gpg_signed_commits/index.md
@@ -0,0 +1 @@
+This document was moved to [another location](../repository/gpg_signed_commits/index.md).
diff --git a/doc/user/project/import/bitbucket.md b/doc/user/project/import/bitbucket.md
new file mode 100644
index 00000000000..b22c7db0047
--- /dev/null
+++ b/doc/user/project/import/bitbucket.md
@@ -0,0 +1,62 @@
+# Import your project from Bitbucket to GitLab
+
+Import your projects from Bitbucket to GitLab with minimal effort.
+
+## Overview
+
+>**Note:**
+The [Bitbucket integration][bb-import] must be first enabled in order to be
+able to import your projects from Bitbucket. Ask your GitLab administrator
+to enable this if not already.
+
+- At its current state, the Bitbucket importer can import:
+ - the repository description (GitLab 7.7+)
+ - the Git repository data (GitLab 7.7+)
+ - the issues (GitLab 7.7+)
+ - the issue comments (GitLab 8.15+)
+ - the pull requests (GitLab 8.4+)
+ - the pull request comments (GitLab 8.15+)
+ - the milestones (GitLab 8.15+)
+ - the wiki (GitLab 8.15+)
+- References to pull requests and issues are preserved (GitLab 8.7+)
+- Repository public access is retained. If a repository is private in Bitbucket
+ it will be created as private in GitLab as well.
+
+
+## How it works
+
+When issues/pull requests are being imported, the Bitbucket importer tries to find
+the Bitbucket author/assignee in GitLab's database using the Bitbucket ID. For this
+to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
+and **associated their Bitbucket account**. If the user is not
+found in GitLab's database, the project creator (most of the times the current
+user that started the import process) is set as the author, but a reference on
+the issue about the original Bitbucket author is kept.
+
+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.
+
+## Importing your Bitbucket repositories
+
+1. Sign in to GitLab and go to your dashboard.
+1. Click on **New project**.
+
+ ![New project in GitLab](img/bitbucket_import_new_project.png)
+
+1. Click on the "Bitbucket" button
+
+ ![Bitbucket](img/import_projects_from_new_project_page.png)
+
+1. Grant GitLab access to your Bitbucket account
+
+ ![Grant access](img/bitbucket_import_grant_access.png)
+
+1. Click on the projects that you'd like to import or **Import all projects**.
+ You can also select the namespace under which each project will be
+ imported.
+
+ ![Import projects](img/bitbucket_import_select_project.png)
+
+[bb-import]: ../../../integration/bitbucket.md
+[social sign-in]: ../../profile/account/social_sign_in.md
diff --git a/doc/user/project/import/clearcase.md b/doc/user/project/import/clearcase.md
new file mode 100644
index 00000000000..f23623ed485
--- /dev/null
+++ b/doc/user/project/import/clearcase.md
@@ -0,0 +1,47 @@
+# Migrating from ClearCase
+
+[ClearCase](https://www-03.ibm.com/software/products/en/clearcase/) is a set of
+tools developed by IBM which also include a centralized version control system
+similar to Git.
+
+A good read of ClearCase's basic concepts is can be found in this [StackOverflow
+post](https://stackoverflow.com/a/645771/974710).
+
+The following table illustrates the main differences between ClearCase and Git:
+
+| Aspect | ClearCase | Git |
+| ------ | --------- | --- |
+| Repository model | Client-server | Distributed |
+| Revision IDs | Branch + number | Global alphanumeric ID |
+| Scope of Change | File | Directory tree snapshot |
+| Concurrency model | Merge | Merge |
+| Storage Method | Deltas | Full content |
+| Client | CLI, Eclipse, CC Client | CLI, Eclipse, Git client/GUIs |
+| Server | UNIX, Windows legacy systems | UNIX, macOS |
+| License | Proprietary | GPL |
+
+_Taken from the slides [ClearCase and the journey to Git](https://www.open.collab.net/media/pdfs/ClearCase-and-the-journey-to-Git.pdf) provided by collab.net_
+
+## Why migrate
+
+ClearCase can be difficult to manage both from a user and an admin perspective.
+Migrating to Git/GitLab there is:
+
+- **No licensing costs**, Git is GPL while ClearCase is proprietary.
+- **Shorter learning curve**, Git has a big community and a vast number of
+ tutorials to get you started.
+- **Integration with modern tools**, migrating to Git and GitLab you can have
+ an open source end-to-end software development platform with built-in version
+ control, issue tracking, code review, CI/CD, and more.
+
+## How to migrate
+
+While there doesn't exist a tool to fully migrate from ClearCase to Git, here
+are some useful links to get you started:
+
+- [Bridge for Git and ClearCase](https://github.com/charleso/git-cc)
+- [Slides "ClearCase and the journey to Git"](https://www.open.collab.net/media/pdfs/ClearCase-and-the-journey-to-Git.pdf)
+- [ClearCase to Git](https://therub.org/2013/07/19/clearcase-to-git/)
+- [Dual syncing ClearCase to Git](https://therub.org/2013/10/22/dual-syncing-clearcase-and-git/)
+- [Moving to Git from ClearCase](https://sateeshkumarb.wordpress.com/2011/01/15/moving-to-git-from-clearcase/)
+- [ClearCase to Git webinar](https://www.brighttalk.com/webcast/11817/162473/clearcase-to-git)
diff --git a/doc/user/project/import/cvs.md b/doc/user/project/import/cvs.md
new file mode 100644
index 00000000000..cabd0eef8d6
--- /dev/null
+++ b/doc/user/project/import/cvs.md
@@ -0,0 +1,68 @@
+# Migrating from CVS
+
+[CVS](https://savannah.nongnu.org/projects/cvs) is an old centralized version
+control system similar to [SVN](svn.md).
+
+## CVS vs Git
+
+The following list illustrates the main differences between CVS and Git:
+
+- **Git is distributed.** On the other hand, CVS is centralized using a client-server
+ architecture. This translates to Git having a more flexible workflow since
+ your working area is a copy of the entire repository. This decreases the
+ overhead when switching branches or merging for example, since you don't have
+ to communicate with a remote server.
+- **Atomic operations.** In Git all operations are
+ [atomic](https://en.wikipedia.org/wiki/Atomic_commit), either they succeed as
+ whole, or they fail without any changes. In CVS, commits (and other operations)
+ are not atomic. If an operation on the repository is interrupted in the middle,
+ the repository can be left in an inconsistent state.
+- **Storage method.** Changes in CVS are per file (changeset), while in Git
+ a committed file(s) is stored in its entirety (snapshot). That means that's
+ very easy in Git to revert or undo a whole change.
+- **Revision IDs.** The fact that in CVS changes are per files, the revision ID
+ is depicted by version numbers, for example `1.4` reflects how many time a
+ given file has been changed. In Git, each version of a project as a whole
+ (each commit) has its unique name given by SHA-1.
+- **Merge tracking.** Git uses a commit-before-merge approach rather than
+ merge-before-commit (or update-then-commit) like CVS. If while you were
+ preparing to create a new commit (new revision) somebody created a
+ new commit on the same branch and pushed to the central repository, CVS would
+ force you to first update your working directory and resolve conflicts before
+ allowing you to commit. This is not the case with Git. You first commit, save
+ your state in version control, then you merge the other developer's changes.
+ You can also ask the other developer to do the merge and resolve any conflicts
+ themselves.
+- **Signed commits.** Git supports signing your commits with GPG for additional
+ security and verification that the commit indeed came from its original author.
+ GitLab can [integrate with GPG](../repository/gpg_signed_commits/index.md)
+ and show whether a signed commit is correctly verified.
+
+_Some of the items above were taken from this great
+[Stack Overflow post](https://stackoverflow.com/a/824241/974710). For a more
+complete list of differences, consult the
+Wikipedia article on [comparing the different version control software](https://en.wikipedia.org/wiki/Comparison_of_version_control_software)._
+
+## Why migrate
+
+CVS is old with no new release since 2008. Git provides more tools to work
+with (`git bisect` for one) which makes for a more productive workflow.
+Migrating to Git/GitLab there is:
+
+- **Shorter learning curve**, Git has a big community and a vast number of
+ tutorials to get you started (see our [Git topic](../../../topics/git/index.md)).
+- **Integration with modern tools**, migrating to Git and GitLab you can have
+ an open source end-to-end software development platform with built-in version
+ control, issue tracking, code review, CI/CD, and more.
+- **Support for many network protocols**. Git supports SSH, HTTP/HTTPS and rsync
+ among others, whereas CVS supports only SSH and its own insecure pserver
+ protocol with no user authentication.
+
+## How to migrate
+
+Here's a few links to get you started with the migration:
+
+- [Migrate using the `cvs-fast-export` tool](http://www.catb.org/~esr/reposurgeon/dvcs-migration-guide.html) ([_source code_](https://gitlab.com/esr/cvs-fast-export))
+- [Stack Overflow post on importing the CVS repo](https://stackoverflow.com/a/11490134/974710)
+- [Convert a CVS repository to Git](http://www.techrepublic.com/blog/linux-and-open-source/convert-cvs-repositories-to-git/)
+- [Man page of the `git-cvsimport` tool](https://www.kernel.org/pub/software/scm/git/docs/git-cvsimport.html)
diff --git a/doc/user/project/import/fogbugz.md b/doc/user/project/import/fogbugz.md
new file mode 100644
index 00000000000..17222c53675
--- /dev/null
+++ b/doc/user/project/import/fogbugz.md
@@ -0,0 +1,28 @@
+# Import your project from FogBugz to GitLab
+
+It only takes a few simple steps to import your project from FogBugz.
+The importer will import all of your cases and comments with original case
+numbers and timestamps. You will also have the opportunity to map FogBugz
+users to GitLab users.
+
+1. From your GitLab dashboard click 'New project'
+1. Click on the 'FogBugz' button
+
+ ![FogBugz](img/fogbugz_import_select_fogbogz.png)
+
+1. Enter your FogBugz URL, email address, and password.
+
+ ![Login](img/fogbugz_import_login.png)
+
+1. Create mapping from FogBugz users to GitLab users.
+
+ ![User Map](img/fogbugz_import_user_map.png)
+
+1. Select the projects you wish to import by clicking the Import buttons
+
+ ![Import Project](img/fogbugz_import_select_project.png)
+
+1. Once the import has finished click the link to take you to the project
+dashboard. Follow the directions to push your existing repository.
+
+ ![Finished](img/fogbugz_import_finished.png)
diff --git a/doc/user/project/import/gitea.md b/doc/user/project/import/gitea.md
new file mode 100644
index 00000000000..f5746a0fb31
--- /dev/null
+++ b/doc/user/project/import/gitea.md
@@ -0,0 +1,77 @@
+# Import your project from Gitea to GitLab
+
+Import your projects from Gitea to GitLab with minimal effort.
+
+## Overview
+
+>**Note:**
+This requires Gitea `v1.0.0` or newer.
+
+- At its current state, Gitea importer can import:
+ - the repository description (GitLab 8.15+)
+ - the Git repository data (GitLab 8.15+)
+ - the issues (GitLab 8.15+)
+ - the pull requests (GitLab 8.15+)
+ - the milestones (GitLab 8.15+)
+ - the labels (GitLab 8.15+)
+- Repository public access is retained. If a repository is private in Gitea
+ it will be created as private in GitLab as well.
+
+## How it works
+
+Since Gitea is currently not an OAuth provider, author/assignee cannot be mapped
+to users in your GitLab's instance. This means that the project creator (most of
+the times the current user that started the import process) is set as the author,
+but a reference on the issue about the original Gitea author is kept.
+
+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.
+
+## Importing your Gitea repositories
+
+The importer page is visible when you create a new project.
+
+![New project page on GitLab](img/import_projects_from_new_project_page.png)
+
+Click on the **Gitea** link and the import authorization process will start.
+
+![New Gitea project import](img/import_projects_from_gitea_new_import.png)
+
+### Authorize access to your repositories using a personal access token
+
+With this method, you will perform a one-off authorization with Gitea to grant
+GitLab access your repositories:
+
+1. Go to <https://you-gitea-instance/user/settings/applications> (replace
+ `you-gitea-instance` with the host of your Gitea instance).
+1. Click **Generate New Token**.
+1. Enter a token description.
+1. Click **Generate Token**.
+1. Copy the token hash.
+1. Go back to GitLab and provide the token to the Gitea importer.
+1. Hit the **List Your Gitea Repositories** button and wait while GitLab reads
+ your repositories' information. Once done, you'll be taken to the importer
+ page to select the repositories to import.
+
+### Select which repositories to import
+
+After you've authorized access to your Gitea repositories, you will be
+redirected to the Gitea importer page.
+
+From there, you can see the import statuses of your Gitea repositories.
+
+- Those that are being imported will show a _started_ status,
+- those already successfully imported will be green with a _done_ status,
+- whereas those that are not yet imported will have an **Import** button on the
+ right side of the table.
+
+If you want, you can import all your Gitea projects in one go by hitting
+**Import all projects** in the upper left corner.
+
+![Gitea importer page](img/import_projects_from_github_importer.png)
+
+---
+
+You can also choose a different name for the project and a different namespace,
+if you have the privileges to do so.
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
new file mode 100644
index 00000000000..016f98966e3
--- /dev/null
+++ b/doc/user/project/import/github.md
@@ -0,0 +1,122 @@
+# Import your project from GitHub to GitLab
+
+Import your projects from GitHub to GitLab with minimal effort.
+
+## Overview
+
+>**Note:**
+If you are an administrator you can enable the [GitHub integration][gh-import]
+in your GitLab instance sitewide. This configuration is optional, users will
+still be able to import their GitHub repositories with a
+[personal access token][gh-token].
+
+>**Note:**
+Administrators of a GitLab instance (Community or Enterprise Edition) can also
+use the [GitHub rake task][gh-rake] to import projects from GitHub without the
+constrains of a Sidekiq worker.
+
+- At its current state, GitHub importer can import:
+ - the repository description (GitLab 7.7+)
+ - the Git repository data (GitLab 7.7+)
+ - the issues (GitLab 7.7+)
+ - the pull requests (GitLab 8.4+)
+ - the wiki pages (GitLab 8.4+)
+ - the milestones (GitLab 8.7+)
+ - the labels (GitLab 8.7+)
+ - the release note descriptions (GitLab 8.12+)
+- References to pull requests and issues are preserved (GitLab 8.7+)
+- Repository public access is retained. If a repository is private in GitHub
+ it will be created as private in GitLab as well.
+
+## How it works
+
+When issues/pull requests are being imported, the GitHub importer tries to find
+the GitHub author/assignee in GitLab's database using the GitHub ID. For this
+to work, the GitHub author/assignee should have signed in beforehand in GitLab
+and **associated their GitHub account**. If the user is not
+found in GitLab's database, the project creator (most of the times the current
+user that started the import process) is set as the author, but a reference on
+the issue about the original GitHub author is kept.
+
+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.
+
+## Importing your GitHub repositories
+
+The importer page is visible when you create a new project.
+
+![New project page on GitLab](img/import_projects_from_new_project_page.png)
+
+Click on the **GitHub** link and the import authorization process will start.
+There are two ways to authorize access to your GitHub repositories:
+
+1. [Using the GitHub integration][gh-integration] (if it's enabled by your
+ GitLab administrator). This is the preferred way as it's possible to
+ preserve the GitHub authors/assignees. Read more in the [How it works](#how-it-works)
+ section.
+1. [Using a personal access token][gh-token] provided by GitHub.
+
+![Select authentication method](img/import_projects_from_github_select_auth_method.png)
+
+### Authorize access to your repositories using the GitHub integration
+
+If the [GitHub integration][gh-import] is enabled by your GitLab administrator,
+you can use it instead of the personal access token.
+
+1. First you may want to connect your GitHub account to GitLab in order for
+ the username mapping to be correct.
+1. Once you connect GitHub, click the **List your GitHub repositories** button
+ and you will be redirected to GitHub for permission to access your projects.
+1. After accepting, you'll be automatically redirected to the importer.
+
+You can now go on and [select which repositories to import](#select-which-repositories-to-import).
+
+### Authorize access to your repositories using a personal access token
+
+>**Note:**
+For a proper author/assignee mapping for issues and pull requests, the
+[GitHub integration][gh-integration] should be used instead of the
+[personal access token][gh-token]. If the GitHub integration is enabled by your
+GitLab administrator, it should be the preferred method to import your repositories.
+Read more in the [How it works](#how-it-works) section.
+
+If you are not using the GitHub integration, you can still perform a one-off
+authorization with GitHub to grant GitLab access your repositories:
+
+1. Go to <https://github.com/settings/tokens/new>.
+1. Enter a token description.
+1. Check the `repo` scope.
+1. Click **Generate token**.
+1. Copy the token hash.
+1. Go back to GitLab and provide the token to the GitHub importer.
+1. Hit the **List Your GitHub Repositories** button and wait while GitLab reads
+ your repositories' information. Once done, you'll be taken to the importer
+ page to select the repositories to import.
+
+### Select which repositories to import
+
+After you've authorized access to your GitHub repositories, you will be
+redirected to the GitHub importer page.
+
+From there, you can see the import statuses of your GitHub repositories.
+
+- Those that are being imported will show a _started_ status,
+- those already successfully imported will be green with a _done_ status,
+- whereas those that are not yet imported will have an **Import** button on the
+ right side of the table.
+
+If you want, you can import all your GitHub projects in one go by hitting
+**Import all projects** in the upper left corner.
+
+![GitHub importer page](img/import_projects_from_github_importer.png)
+
+---
+
+You can also choose a different name for the project and a different namespace,
+if you have the privileges to do so.
+
+[gh-import]: ../../../integration/github.md "GitHub integration"
+[gh-rake]: ../../../administration/raketasks/github_import.md "GitHub rake task"
+[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
+[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
diff --git a/doc/user/project/import/gitlab_com.md b/doc/user/project/import/gitlab_com.md
new file mode 100644
index 00000000000..3b37da67a5b
--- /dev/null
+++ b/doc/user/project/import/gitlab_com.md
@@ -0,0 +1,20 @@
+# Project importing from GitLab.com to your private GitLab instance
+
+You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if
+GitLab support is enabled on your GitLab instance.
+You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html)
+To get to the importer page you need to go to "New project" page.
+
+>**Note:**
+If you are interested in importing Wiki and Merge Request data to your new
+instance, you'll need to follow the instructions for [project export](../settings/import_export.md)
+
+![New project page](img/gitlab_new_project_page.png)
+
+Click on the "Import projects from GitLab.com" link and you will be redirected to GitLab.com
+for permission to access your projects. After accepting, you'll be automatically redirected to the importer.
+
+![Importer page](img/gitlab_importer.png)
+
+To import a project, you can simple click "Import". The importer will import your repository and issues.
+Once the importer is done, a new GitLab project will be created with your imported data.
diff --git a/doc/workflow/importing/img/bitbucket_import_grant_access.png b/doc/user/project/import/img/bitbucket_import_grant_access.png
index 429904e621d..429904e621d 100644
--- a/doc/workflow/importing/img/bitbucket_import_grant_access.png
+++ b/doc/user/project/import/img/bitbucket_import_grant_access.png
Binary files differ
diff --git a/doc/workflow/importing/img/bitbucket_import_new_project.png b/doc/user/project/import/img/bitbucket_import_new_project.png
index 8ed528c2f09..8ed528c2f09 100644
--- a/doc/workflow/importing/img/bitbucket_import_new_project.png
+++ b/doc/user/project/import/img/bitbucket_import_new_project.png
Binary files differ
diff --git a/doc/workflow/importing/img/bitbucket_import_select_project.png b/doc/user/project/import/img/bitbucket_import_select_project.png
index 1bca6166ec8..1bca6166ec8 100644
--- a/doc/workflow/importing/img/bitbucket_import_select_project.png
+++ b/doc/user/project/import/img/bitbucket_import_select_project.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png b/doc/user/project/import/img/fogbugz_import_finished.png
index 62c5c86c9b3..62c5c86c9b3 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png
+++ b/doc/user/project/import/img/fogbugz_import_finished.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png b/doc/user/project/import/img/fogbugz_import_login.png
index 96bce70b74d..96bce70b74d 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png
+++ b/doc/user/project/import/img/fogbugz_import_login.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png b/doc/user/project/import/img/fogbugz_import_select_fogbogz.png
index b26c652e382..b26c652e382 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png
+++ b/doc/user/project/import/img/fogbugz_import_select_fogbogz.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png b/doc/user/project/import/img/fogbugz_import_select_project.png
index ccc82f9d4cd..ccc82f9d4cd 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png
+++ b/doc/user/project/import/img/fogbugz_import_select_project.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png b/doc/user/project/import/img/fogbugz_import_user_map.png
index 28ff55a8d89..28ff55a8d89 100644
--- a/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png
+++ b/doc/user/project/import/img/fogbugz_import_user_map.png
Binary files differ
diff --git a/doc/workflow/importing/gitlab_importer/importer.png b/doc/user/project/import/img/gitlab_importer.png
index 27d42eb492e..27d42eb492e 100644
--- a/doc/workflow/importing/gitlab_importer/importer.png
+++ b/doc/user/project/import/img/gitlab_importer.png
Binary files differ
diff --git a/doc/workflow/importing/gitlab_importer/new_project_page.png b/doc/user/project/import/img/gitlab_new_project_page.png
index c673724f436..c673724f436 100644
--- a/doc/workflow/importing/gitlab_importer/new_project_page.png
+++ b/doc/user/project/import/img/gitlab_new_project_page.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_gitea_new_import.png b/doc/user/project/import/img/import_projects_from_gitea_new_import.png
index a3f603cbd0a..a3f603cbd0a 100644
--- a/doc/workflow/importing/img/import_projects_from_gitea_new_import.png
+++ b/doc/user/project/import/img/import_projects_from_gitea_new_import.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_importer.png b/doc/user/project/import/img/import_projects_from_github_importer.png
index d8effaf6075..d8effaf6075 100644
--- a/doc/workflow/importing/img/import_projects_from_github_importer.png
+++ b/doc/user/project/import/img/import_projects_from_github_importer.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png b/doc/user/project/import/img/import_projects_from_github_select_auth_method.png
index 1ccb38a815e..1ccb38a815e 100644
--- a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
+++ b/doc/user/project/import/img/import_projects_from_github_select_auth_method.png
Binary files differ
diff --git a/doc/workflow/importing/img/import_projects_from_new_project_page.png b/doc/user/project/import/img/import_projects_from_new_project_page.png
index 97ca30b2087..97ca30b2087 100644
--- a/doc/workflow/importing/img/import_projects_from_new_project_page.png
+++ b/doc/user/project/import/img/import_projects_from_new_project_page.png
Binary files differ
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
new file mode 100644
index 00000000000..8da6e2a8207
--- /dev/null
+++ b/doc/user/project/import/index.md
@@ -0,0 +1,24 @@
+# Migrating projects to a GitLab instance
+
+1. [From Bitbucket.org](bitbucket.md)
+1. [From ClearCase](clearcase.md)
+1. [From CVS](cvs.md)
+1. [From FogBugz](fogbugz.md)
+1. [From GitHub.com of GitHub Enterprise](github.md)
+1. [From GitLab.com](gitlab_com.md)
+1. [From Gitea](gitea.md)
+1. [From Perforce](perforce.md)
+1. [From SVN](svn.md)
+1. [From TFS](tfs.md)
+
+In addition to the specific migration documentation above, you can import any
+Git repository via HTTP from the New Project page. Be aware that if the
+repository is too large the import can timeout.
+
+## Migrating from self-hosted GitLab to GitLab.com
+
+You can copy your repos by changing the remote and pushing to the new server,
+but issues and merge requests can't be imported.
+
+If you want to retain all metadata like issues and merge requests, you can use
+the [import/export feature](../settings/import_export.md).
diff --git a/doc/user/project/import/perforce.md b/doc/user/project/import/perforce.md
new file mode 100644
index 00000000000..aa7508e1e8e
--- /dev/null
+++ b/doc/user/project/import/perforce.md
@@ -0,0 +1,50 @@
+# Migrating from Perforce Helix
+
+[Perforce Helix](https://www.perforce.com/) provides a set of tools which also
+include a centralized, proprietary version control system similar to Git.
+
+## Perforce vs Git
+
+The following list illustrates the main differences between Perforce Helix and
+Git:
+
+1. In general the biggest difference is that Perforce branching is heavyweight
+ compared to Git's lightweight branching. When you create a branch in Perforce,
+ it creates an integration record in their proprietary database for every file
+ in the branch, regardless how many were actually changed. Whereas Git was
+ implemented with a different architecture so that a single SHA acts as a pointer
+ to the state of the whole repo after the changes, making it very easy to branch.
+ This is what made feature branching workflows so easy to adopt with Git.
+1. Also, context switching between branches is much easier in Git. If your manager
+ said 'You need to stop work on that new feature and fix this security
+ vulnerability' you can do so very easily in Git.
+1. Having a complete copy of the project and its history on your local machine
+ means every transaction is superfast and Git provides that. You can branch/merge
+ and experiment in isolation, then clean up your mess before sharing your new
+ cool stuff with everyone.
+1. Git also made code review simple because you could share your changes without
+ merging them to master, whereas Perforce had to implement a Shelving feature on
+ the server so others could review changes before merging.
+
+## Why migrate
+
+Perforce Helix can be difficult to manage both from a user and an admin
+perspective. Migrating to Git/GitLab there is:
+
+- **No licensing costs**, Git is GPL while Perforce Helix is proprietary.
+- **Shorter learning curve**, Git has a big community and a vast number of
+ tutorials to get you started.
+- **Integration with modern tools**, migrating to Git and GitLab you can have
+ an open source end-to-end software development platform with built-in version
+ control, issue tracking, code review, CI/CD, and more.
+
+## How to migrate
+
+Git includes a built-in mechanism (`git p4`) to pull code from Perforce and to
+submit back from Git to Perforce.
+
+Here's a few links to get you started:
+
+- [git-p4 manual page](https://www.kernel.org/pub/software/scm/git/docs/git-p4.html)
+- [git-p4 example usage](https://git.wiki.kernel.org/index.php/Git-p4_Usage)
+- [Git book migration guide](https://git-scm.com/book/en/v2/Git-and-Other-Systems-Migrating-to-Git#_perforce_import)
diff --git a/doc/user/project/import/svn.md b/doc/user/project/import/svn.md
new file mode 100644
index 00000000000..7a3628a39d7
--- /dev/null
+++ b/doc/user/project/import/svn.md
@@ -0,0 +1,183 @@
+# Migrating from SVN to GitLab
+
+Subversion (SVN) is a central version control system (VCS) while
+Git is a distributed version control system. There are some major differences
+between the two, for more information consult your favorite search engine.
+
+## Overview
+
+There are two approaches to SVN to Git migration:
+
+1. [Git/SVN Mirror](#smooth-migration-with-a-gitsvn-mirror-using-subgit) which:
+ - Makes the GitLab repository to mirror the SVN project.
+ - Git and SVN repositories are kept in sync; you can use either one.
+ - Smoothens the migration process and allows to manage migration risks.
+
+1. [Cut over migration](#cut-over-migration-with-svn2git) which:
+ - Translates and imports the existing data and history from SVN to Git.
+ - Is a fire and forget approach, good for smaller teams.
+
+## Smooth migration with a Git/SVN mirror using SubGit
+
+[SubGit](https://subgit.com) is a tool for a smooth, stress-free SVN to Git
+migration. It creates a writable Git mirror of a local or remote Subversion
+repository and that way you can use both Subversion and Git as long as you like.
+It requires access to your GitLab server as it talks with the Git repositories
+directly in a filesystem level.
+
+### SubGit prerequisites
+
+1. Install Oracle JRE 1.8 or newer. On Debian-based Linux distributions you can
+ follow [this article](http://www.webupd8.org/2012/09/install-oracle-java-8-in-ubuntu-via-ppa.html).
+1. Download SubGit from https://subgit.com/download/.
+1. Unpack the downloaded SubGit zip archive to the `/opt` directory. The `subgit`
+ command will be available at `/opt/subgit-VERSION/bin/subgit`.
+
+### SubGit configuration
+
+The first step to mirror you SVN repository in GitLab is to create a new empty
+project which will be used as a mirror. For Omnibus installations the path to
+the repository will be located at
+`/var/opt/gitlab/git-data/repositories/USER/REPO.git` by default. For
+installations from source, the default repository directory will be
+`/home/git/repositories/USER/REPO.git`. For convenience, assign this path to a
+variable:
+
+```
+GIT_REPO_PATH=/var/opt/gitlab/git-data/repositories/USER/REPOS.git
+```
+
+SubGit will keep this repository in sync with a remote SVN project. For
+convenience, assign your remote SVN project URL to a variable:
+
+```
+SVN_PROJECT_URL=http://svn.company.com/repos/project
+```
+
+Next you need to run SubGit to set up a Git/SVN mirror. Make sure the following
+`subgit` command is ran on behalf of the same user that keeps ownership of
+GitLab Git repositories (by default `git`):
+
+```
+subgit configure --layout auto $SVN_PROJECT_URL $GIT_REPO_PATH
+```
+
+Adjust authors and branches mappings, if necessary. Open with your favorite
+text editor:
+
+```
+edit $GIT_REPO_PATH/subgit/authors.txt
+edit $GIT_REPO_PATH/subgit/config
+```
+
+For more information regarding the SubGit configuration options, refer to
+[SubGit's documentation](https://subgit.com/documentation.html) website.
+
+### Initial translation
+
+Now that SubGit has configured the Git/SVN repos, run `subgit` to perform the
+initial translation of existing SVN revisions into the Git repository:
+
+```
+subgit install $GIT_REPO_PATH
+```
+
+After the initial translation is completed, the Git repository and the SVN
+project will be kept in sync by `subgit` - new Git commits will be translated to
+SVN revisions and new SVN revisions will be translated to Git commits. Mirror
+works transparently and does not require any special commands.
+
+If you would prefer to perform one-time cut over migration with `subgit`, use
+the `import` command instead of `install`:
+
+```
+subgit import $GIT_REPO_PATH
+```
+
+### SubGit licensing
+
+Running SubGit in a mirror mode requires a
+[registration](https://subgit.com/pricing.html). Registration is free for open
+source, academic and startup projects.
+
+We're currently working on deeper GitLab/SubGit integration. You may track our
+progress at [this issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/990).
+
+### SubGit support
+
+For any questions related to SVN to GitLab migration with SubGit, you can
+contact the SubGit team directly at [support@subgit.com](mailto:support@subgit.com).
+
+## Cut over migration with svn2git
+
+If you are currently using an SVN repository, you can migrate the repository
+to Git and GitLab. We recommend a hard cut over - run the migration command once
+and then have all developers start using the new GitLab repository immediately.
+Otherwise, it's hard to keep changing in sync in both directions. The conversion
+process should be run on a local workstation.
+
+Install `svn2git`. On all systems you can install as a Ruby gem if you already
+have Ruby and Git installed.
+
+```bash
+sudo gem install svn2git
+```
+
+On Debian-based Linux distributions you can install the native packages:
+
+```bash
+sudo apt-get install git-core git-svn ruby
+```
+
+Optionally, prepare an authors file so `svn2git` can map SVN authors to Git authors.
+If you choose not to create the authors file then commits will not be attributed
+to the correct GitLab user. Some users may not consider this a big issue while
+others will want to ensure they complete this step. If you choose to map authors
+you will be required to map every author that is present on changes in the SVN
+repository. If you don't, the conversion will fail and you will have to update
+the author file accordingly. The following command will search through the
+repository and output a list of authors.
+
+```bash
+svn log --quiet | grep -E "r[0-9]+ \| .+ \|" | cut -d'|' -f2 | sed 's/ //g' | sort | uniq
+```
+
+Use the output from the last command to construct the authors file.
+Create a file called `authors.txt` and add one mapping per line.
+
+```
+janedoe = Jane Doe <janedoe@example.com>
+johndoe = John Doe <johndoe@example.com>
+```
+
+If your SVN repository is in the standard format (trunk, branches, tags,
+not nested) the conversion is simple. For a non-standard repository see
+[svn2git documentation](https://github.com/nirvdrum/svn2git). The following
+command will checkout the repository and do the conversion in the current
+working directory. Be sure to create a new directory for each repository before
+running the `svn2git` command. The conversion process will take some time.
+
+```bash
+svn2git https://svn.example.com/path/to/repo --authors /path/to/authors.txt
+```
+
+If your SVN repository requires a username and password add the
+`--username <username>` and `--password <password` flags to the above command.
+`svn2git` also supports excluding certain file paths, branches, tags, etc. See
+[svn2git documentation](https://github.com/nirvdrum/svn2git) or run
+`svn2git --help` for full documentation on all of the available options.
+
+Create a new GitLab project, where you will eventually push your converted code.
+Copy the SSH or HTTP(S) repository URL from the project page. Add the GitLab
+repository as a Git remote and push all the changes. This will push all commits,
+branches and tags.
+
+```bash
+git remote add origin git@gitlab.com:<group>/<project>.git
+git push --all origin
+git push --tags origin
+```
+
+## Contribute to this guide
+We welcome all contributions that would expand this guide with instructions on
+how to migrate from SVN and other version control systems.
diff --git a/doc/user/project/import/tfs.md b/doc/user/project/import/tfs.md
new file mode 100644
index 00000000000..8727c2ff6c3
--- /dev/null
+++ b/doc/user/project/import/tfs.md
@@ -0,0 +1,42 @@
+# Migrating from TFS
+
+[TFS](https://www.visualstudio.com/tfs/) is a set of tools developed by Microsoft
+which also includes a centralized version control system (TFVC) similar to Git.
+
+In this document, we emphasize on the TFVC to Git migration.
+
+## TFVC vs Git
+
+The following list illustrates the main differences between TFVC and Git:
+
+- **Git is distributed** whereas TFVC is centralized using a client-server
+ architecture. This translates to Git having a more flexible workflow since
+ your working area is a copy of the entire repository. This decreases the
+ overhead when switching branches or merging for example, since you don't have
+ to communicate with a remote server.
+- **Storage method.** Changes in CVS are per file (changeset), while in Git
+ a committed file(s) is stored in its entirety (snapshot). That means that's
+ very easy in Git to revert or undo a whole change.
+
+_Check also Microsoft's documentation on the
+[comparison of Git and TFVC](https://www.visualstudio.com/en-us/docs/tfvc/comparison-git-tfvc)
+and the Wikipedia article on
+[comparing the different version control software](https://en.wikipedia.org/wiki/Comparison_of_version_control_software)._
+
+## Why migrate
+
+Migrating to Git/GitLab there is:
+
+- **No licensing costs**, Git is GPL while TFVC is proprietary.
+- **Shorter learning curve**, Git has a big community and a vast number of
+ tutorials to get you started (see our [Git topic](../../../topics/git/index.md)).
+- **Integration with modern tools**, migrating to Git and GitLab you can have
+ an open source end-to-end software development platform with built-in version
+ control, issue tracking, code review, CI/CD, and more.
+
+## How to migrate
+
+The best option to migrate from TFVC to Git is to use the
+[`git-tfs`](https://github.com/git-tfs/git-tfs) tool. A specific guide for the
+migration exists:
+[Migrate TFS to Git](https://github.com/git-tfs/git-tfs/blob/master/doc/usecases/migrate_tfs_to_git.md).
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 91a19600951..d6b3d59d407 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -24,6 +24,7 @@ integrated platform
from messing with history or pushing code without review
- [Protected tags](protected_tags.md): Control over who has
permission to create tags, and prevent accidental update or deletion
+ - [Signing commits](gpg_signed_commits/index.md): use GPG to sign your commits
- [Merge Requests](merge_requests/index.md): Apply your branching
strategy and get reviewed by your team
- [Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) (**EES/EEP**): Ask for approval before
@@ -66,8 +67,6 @@ website with GitLab Pages
**Other features:**
- [Cycle Analytics](cycle_analytics.md): Review your development lifecycle
-- [Koding integration](koding.md) (not available on GitLab.com): Integrate
-with Koding to have access to a web terminal right from the GitLab UI
- [Syntax highlighting](highlighting.md): An alternative to customize
your code blocks, overriding GitLab's default choice of language
@@ -90,15 +89,19 @@ from your fork to the upstream project
## Import or export a project
-- Import a project from:
- - [GitHub to GitLab](../../workflow/importing/import_projects_from_github.md)
- - [BitBucket to GitLab](../../workflow/importing/import_projects_from_bitbucket.md)
- - [Gitea to GitLab](../../workflow/importing/import_projects_from_gitea.md)
- - [FogBugz to GitLab](../../workflow/importing/import_projects_from_fogbugz.md)
+- [Import a project](import/index.md) from:
+ - [GitHub to GitLab](import/github.md)
+ - [BitBucket to GitLab](import/bitbucket.md)
+ - [Gitea to GitLab](import/gitea.md)
+ - [FogBugz to GitLab](import/fogbugz.md)
- [Export a project from GitLab](settings/import_export.md#exporting-a-project-and-its-data)
- [Importing and exporting projects between GitLab instances](settings/import_export.md)
-## Leave a project
+## Project's members
+
+Learn how to [add members to your projects](members/index.md).
+
+### Leave a project
**Leave project** will only display on the project's dashboard
when a project is part of a group (under a
diff --git a/doc/user/project/integrations/img/jira_service_page.png b/doc/user/project/integrations/img/jira_service_page.png
index e69376f74c4..63aa0e99a50 100644
--- a/doc/user/project/integrations/img/jira_service_page.png
+++ b/doc/user/project/integrations/img/jira_service_page.png
Binary files differ
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index 4f583879a4e..93aec56f8dc 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -10,7 +10,12 @@ JIRA](https://www.programmableweb.com/news/how-and-why-to-integrate-gitlab-jira/
## Configuration
-Each GitLab project can be configured to connect to a different JIRA instance.
+Each GitLab project can be configured to connect to a different JIRA instance. That
+means one GitLab project maps to _all_ JIRA projects in that JIRA instance once
+the configuration is set up. Therefore, you don't have to explicitly associate
+one GitLab project to any JIRA project. Once the configuration is set up, any JIRA
+projects in the JIRA instance are already mapped to the GitLab project.
+
If you have one JIRA instance you can pre-fill the settings page with a default
template, see the [Services Templates][services-templates] docs.
@@ -103,7 +108,6 @@ in the table below.
| ----- | ----------- |
| `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
| `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. |
-| `Project key` | Put a JIRA project key (in uppercase), e.g. `MARS` in this field. This is only for testing the configuration settings. JIRA integration in GitLab works with _all_ JIRA projects in your JIRA instance. This field will be removed in a future release. |
| `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
| `Transition ID` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 6f15765751c..5fefb3b69c4 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -40,7 +40,7 @@ Installing and configuring Prometheus to monitor applications is fairly straight
### Configuring Omnibus GitLab Prometheus to monitor Kubernetes deployments
With Omnibus GitLab running inside of Kubernetes, you can leverage the bundled
-version of Prometheus to collect the supported metrics. Once enabled, Prometheus will automatically begin monitoring Kubernetes Nodes and any [annotated Pods](https://prometheus.io/docs/operating/configuration/#<kubernetes_sd_config>).
+version of Prometheus to collect the supported metrics. Once enabled, Prometheus will automatically begin monitoring Kubernetes Nodes and any [annotated Pods](https://prometheus.io/docs/operating/configuration/#<kubernetes_sd_config>).
1. Read how to configure the bundled Prometheus server in the
[Administration guide][gitlab-prometheus-k8s-monitor].
@@ -133,6 +133,8 @@ to integrate with.
Once configured, GitLab will attempt to retrieve performance metrics for any
environment which has had a successful deployment.
+GitLab will automatically scan the Prometheus server for known metrics and attempt to identify the metrics for a particular environment. The supported metrics and scan process is detailed in our [Prometheus Metric Library documentation](prometheus_library/metrics.html).
+
[Learn more about monitoring environments.](../../../ci/environments.md#monitoring-environments)
## Determining the performance impact of a merge
@@ -174,7 +176,7 @@ If the "Attempting to load performance data" screen continues to appear, it coul
[prometheus-docker-image]: https://hub.docker.com/r/prom/prometheus/
[prometheus-yml]:samples/prometheus.yml
[gitlab.com-ip-range]: https://gitlab.com/gitlab-com/infrastructure/issues/434
-[ci-environment-slug]: https://docs.gitlab.com/ce/ci/variables/#predefined-variables-environment-variables
+[ci-environment-slug]: ../../../ci/variables/#predefined-variables-environment-variables
[ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935
[ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408
[promgldocs]: ../../../administration/monitoring/prometheus/index.md
diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md
index eb8cd821ddc..9f0308d8111 100644
--- a/doc/user/project/integrations/prometheus_library/kubernetes.md
+++ b/doc/user/project/integrations/prometheus_library/kubernetes.md
@@ -23,4 +23,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][autodeploy] 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/metrics.md b/doc/user/project/integrations/prometheus_library/metrics.md
index 6bdffce9c55..f09ecf9ff2d 100644
--- a/doc/user/project/integrations/prometheus_library/metrics.md
+++ b/doc/user/project/integrations/prometheus_library/metrics.md
@@ -4,6 +4,7 @@
GitLab offers automatic detection of select [Prometheus exporters](https://prometheus.io/docs/instrumenting/exporters/). Currently supported exporters are:
* [Kubernetes](kubernetes.md)
* [NGINX](nginx.md)
+* [NGINX Ingress Controller](nginx_ingress.md)
* [HAProxy](haproxy.md)
* [Amazon Cloud Watch](cloudwatch.md)
@@ -14,10 +15,7 @@ We have tried to surface the most important metrics for each exporter, and will
GitLab retrieves performance data from the configured Prometheus server, and attempts to identifying the presence of known metrics. Once identified, GitLab then needs to be able to map the data to a particular environment.
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 that,
-GitLab will look for the required metrics which have a label that
-matches the [$CI_ENVIRONMENT_SLUG][ci-environment-slug].
-
-For example if you are deploying to an environment named `production`, there must be a label for the metric with the value of `production`.
+GitLab uses the defined queries and fills in the environment specific variables. Typically this involves looking for the [$CI_ENVIRONMENT_SLUG](https://docs.gitlab.com/ee/ci/variables/#predefined-variables-environment-variables), but may also include other information such as the project's Kubernetes namespace. Each search query is defined in the [exporter specific documentation](#prometheus-metrics-library).
## Adding to the library
diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md
index b3470773996..12e3321f5f3 100644
--- a/doc/user/project/integrations/prometheus_library/nginx.md
+++ b/doc/user/project/integrations/prometheus_library/nginx.md
@@ -8,8 +8,8 @@ GitLab has support for automatically detecting and monitoring NGINX. This is pro
| Name | Query |
| ---- | ----- |
| Throughput (req/sec) | sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) |
-| Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) * 1000 |
-| HTTP Error Rate (%) | sum(rate(haproxy_frontend_http_responses_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_responses_total{%{environment_filter}}[2m])) |
+| 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])) |
## Configuring Prometheus to monitor for NGINX metrics
diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
new file mode 100644
index 00000000000..84ee8bc45e5
--- /dev/null
+++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
@@ -0,0 +1,25 @@
+# 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.
+
+## Metrics supported
+
+| Name | Query |
+| ---- | ----- |
+| Throughput (req/sec) | sum(rate(nginx_upstream_requests_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) |
+| 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
+
+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.
+
+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.
+
+## 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>-*`.
+
+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/issues/confidential_issues.md b/doc/user/project/issues/confidential_issues.md
index 1760b182114..0bf1f396f9d 100644
--- a/doc/user/project/issues/confidential_issues.md
+++ b/doc/user/project/issues/confidential_issues.md
@@ -9,7 +9,7 @@ keep security vulnerabilities private or prevent surprises from leaking out.
## Making an issue confidential
-You can make an issue confidential either by creating a new issue or editing
+You can make an issue confidential during issue creation or by editing
an existing one.
When you create a new issue, a checkbox right below the text area is available
@@ -19,11 +19,19 @@ confidential checkbox and hit **Save changes**.
![Creating a new confidential issue](img/confidential_issues_create.png)
-## Making an issue non-confidential
+## Modifying issue confidentiality
-To make an issue non-confidential, all you have to do is edit it and unmark
-the confidential checkbox. Once you save the issue, it will gain the default
-visibility level you have chosen for your project.
+There are two ways to change an issue's confidentiality.
+
+The first way is to edit the issue and mark/unmark the confidential checkbox.
+Once you save the issue, it will change the confidentiality of the issue.
+
+The second way is to locate the Confidentiality section in the sidebar and click
+**Edit**. A popup should appear and give you the option to turn on or turn off confidentiality.
+
+| Turn off confidentiality | Turn on confidentiality |
+| :-----------: | :----------: |
+| ![Turn off confidentiality](img/turn_off_confidentiality.png) | ![Turn on confidentiality](img/turn_on_confidentiality.png) |
Every change from regular to confidential and vice versa, is indicated by a
system note in the issue's comments.
@@ -49,6 +57,12 @@ issue you are commenting on is confidential.
![Confidential issue page](img/confidential_issues_issue_page.png)
+There is also an indicator on the sidebar denoting confidentiality.
+
+| Confidential issue | Not confidential issue |
+| :-----------: | :----------: |
+| ![Sidebar confidential issue](img/sidebar_confidential_issue.png) | ![Sidebar not confidential issue](img/sidebar_not_confidential_issue.png) |
+
## Permissions and access to confidential issues
There are two kinds of level access for confidential issues. The general rule
diff --git a/doc/user/project/issues/img/confidential_issues_index_page.png b/doc/user/project/issues/img/confidential_issues_index_page.png
index e4b492a2769..f3efe0ce04e 100755..100644
--- a/doc/user/project/issues/img/confidential_issues_index_page.png
+++ b/doc/user/project/issues/img/confidential_issues_index_page.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_issue_page.png b/doc/user/project/issues/img/confidential_issues_issue_page.png
index f04ec8ff32b..0f5c774d258 100755..100644
--- a/doc/user/project/issues/img/confidential_issues_issue_page.png
+++ b/doc/user/project/issues/img/confidential_issues_issue_page.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_system_notes.png b/doc/user/project/issues/img/confidential_issues_system_notes.png
index 82e0dd8e85e..355be80ecb6 100755..100644
--- a/doc/user/project/issues/img/confidential_issues_system_notes.png
+++ b/doc/user/project/issues/img/confidential_issues_system_notes.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
new file mode 100644
index 00000000000..5d20e8cbc89
--- /dev/null
+++ b/doc/user/project/issues/img/group_issues_list_view.png
Binary files differ
diff --git a/doc/user/project/issues/img/issue_tracker.png b/doc/user/project/issues/img/issue_tracker.png
deleted file mode 100755
index ab25cb64d13..00000000000
--- a/doc/user/project/issues/img/issue_tracker.png
+++ /dev/null
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
new file mode 100644
index 00000000000..2fcc9e8d9da
--- /dev/null
+++ 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
new file mode 100755
index 00000000000..d99a1ca756e
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..111f7861364
--- /dev/null
+++ 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
new file mode 100755
index 00000000000..2e6cbbc5b3a
--- /dev/null
+++ b/doc/user/project/issues/img/sidebar_not_confidential_issue.png
Binary files differ
diff --git a/doc/user/project/issues/img/turn_off_confidentiality.png b/doc/user/project/issues/img/turn_off_confidentiality.png
new file mode 100644
index 00000000000..248ae6522d6
--- /dev/null
+++ b/doc/user/project/issues/img/turn_off_confidentiality.png
Binary files differ
diff --git a/doc/user/project/issues/img/turn_on_confidentiality.png b/doc/user/project/issues/img/turn_on_confidentiality.png
new file mode 100644
index 00000000000..fac4c833699
--- /dev/null
+++ b/doc/user/project/issues/img/turn_on_confidentiality.png
Binary files differ
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 1f78849a92c..0f187946a4a 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -7,7 +7,7 @@ of solving a problem.
It allows you, your team, and your collaborators to share
and discuss proposals before and while implementing them.
-Issues and the GitLab Issue Tracker are available in all
+GitLab Issues and the GitLab Issue Tracker are available in all
[GitLab Products](https://about.gitlab.com/products/) as
part of the [GitLab Workflow](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
@@ -48,11 +48,27 @@ for feature proposals and another one for bug reports.
## Issue Tracker
-The issue tracker is the collection of opened and closed issues created in a project.
+The Issue Tracker is the collection of opened and closed issues created in a project.
+It is available for all projects, from the moment the project is created.
-![Issue tracker](img/issue_tracker.png)
+Find the issue tracker by navigating to your **Project's homepage** > **Issues**.
-Find the issue tracker by navigating to your **Project's Dashboard** > **Issues**.
+### Issues per project
+
+When you access your project's issues, GitLab will present them in a list,
+and you can use the tabs available to quickly filter by open and closed issues.
+
+![Project issues list view](img/project_issues_list_view.png)
+
+You can also [search and filter](../../search/index.md#issues-and-merge-requests-per-project) the results more deeply with GitLab's search capacities.
+
+### Issues per group
+
+View all the issues in a group (that is, all the issues across all projects in that
+group) by navigating to **Group > Issues**. This view also has the open and closed
+issue tabs.
+
+![Group Issues list view](img/group_issues_list_view.png)
## GitLab Issues Functionalities
@@ -70,6 +86,10 @@ Read through the [documentation on creating issues](create_new_issue.md).
Learn distinct ways to [close issues](closing_issues.md) in GitLab.
+## Moving issues
+
+Read through the [documentation on moving issues](moving_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).
@@ -120,6 +140,12 @@ to find out more about this feature.
With [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/), you can also
create various boards per project with [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards).
+### External Issue Tracker
+
+Alternatively to GitLab's built-in Issue Tracker, you can also use an [external
+tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine,
+or Bugzilla.
+
### Issue's API
Read through the [API documentation](../../../api/issues.md).
diff --git a/doc/user/project/issues/moving_issues.md b/doc/user/project/issues/moving_issues.md
new file mode 100644
index 00000000000..211a651b89e
--- /dev/null
+++ b/doc/user/project/issues/moving_issues.md
@@ -0,0 +1,10 @@
+# Moving Issues
+
+Please read through the [GitLab Issue Documentation](index.md) for an overview on GitLab Issues.
+
+Moving an issue will close it and duplicate it on the specified project.
+There will also be a system note added to both issues indicating where it came from or went to.
+
+You can move an issue with the "Move issue" button at the bottom of the right-sidebar when viewing the issue.
+
+![move issue - button](img/sidebar_move_issue.png)
diff --git a/doc/user/project/koding.md b/doc/user/project/koding.md
index 455e2ee47b4..86e06a39e59 100644
--- a/doc/user/project/koding.md
+++ b/doc/user/project/koding.md
@@ -1,6 +1,9 @@
# Koding integration
-> [Introduced][ce-5909] in GitLab 8.11.
+>**Notes:**
+- **As of GitLab 10.0, the Koding integration is deprecated and will be removed
+ in a future version.**
+- [Introduced][ce-5909] in GitLab 8.11.
This document will guide you through using Koding integration on GitLab in
detail. For configuring and installing please follow the
diff --git a/doc/workflow/add-user/img/access_requests_management.png b/doc/user/project/members/img/access_requests_management.png
index 3693bed869b..3693bed869b 100644
--- a/doc/workflow/add-user/img/access_requests_management.png
+++ b/doc/user/project/members/img/access_requests_management.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_new_user_to_project_settings.png b/doc/user/project/members/img/add_new_user_to_project_settings.png
index 40db600455f..40db600455f 100644
--- a/doc/workflow/add-user/img/add_new_user_to_project_settings.png
+++ b/doc/user/project/members/img/add_new_user_to_project_settings.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_accept.png b/doc/user/project/members/img/add_user_email_accept.png
index 763b3ff463d..763b3ff463d 100644
--- a/doc/workflow/add-user/img/add_user_email_accept.png
+++ b/doc/user/project/members/img/add_user_email_accept.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_ready.png b/doc/user/project/members/img/add_user_email_ready.png
index 0066eb3427b..0066eb3427b 100644
--- a/doc/workflow/add-user/img/add_user_email_ready.png
+++ b/doc/user/project/members/img/add_user_email_ready.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_search.png b/doc/user/project/members/img/add_user_email_search.png
index 66bcd6aad80..66bcd6aad80 100644
--- a/doc/workflow/add-user/img/add_user_email_search.png
+++ b/doc/user/project/members/img/add_user_email_search.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_give_permissions.png b/doc/user/project/members/img/add_user_give_permissions.png
index 376a3eefccc..376a3eefccc 100644
--- a/doc/workflow/add-user/img/add_user_give_permissions.png
+++ b/doc/user/project/members/img/add_user_give_permissions.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png b/doc/user/project/members/img/add_user_import_members_from_another_project.png
index 0c32001098e..0c32001098e 100644
--- a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
+++ b/doc/user/project/members/img/add_user_import_members_from_another_project.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_imported_members.png b/doc/user/project/members/img/add_user_imported_members.png
index 51fd7688890..51fd7688890 100644
--- a/doc/workflow/add-user/img/add_user_imported_members.png
+++ b/doc/user/project/members/img/add_user_imported_members.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_list_members.png b/doc/user/project/members/img/add_user_list_members.png
index e0fa404288d..e0fa404288d 100644
--- a/doc/workflow/add-user/img/add_user_list_members.png
+++ b/doc/user/project/members/img/add_user_list_members.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_members_menu.png b/doc/user/project/members/img/add_user_members_menu.png
index 8e61d15fe65..8e61d15fe65 100644
--- a/doc/workflow/add-user/img/add_user_members_menu.png
+++ b/doc/user/project/members/img/add_user_members_menu.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_search_people.png b/doc/user/project/members/img/add_user_search_people.png
index 41767a9167c..41767a9167c 100644
--- a/doc/workflow/add-user/img/add_user_search_people.png
+++ b/doc/user/project/members/img/add_user_search_people.png
Binary files differ
diff --git a/doc/workflow/groups/max_access_level.png b/doc/user/project/members/img/max_access_level.png
index 63f33f9d91d..63f33f9d91d 100644
--- a/doc/workflow/groups/max_access_level.png
+++ b/doc/user/project/members/img/max_access_level.png
Binary files differ
diff --git a/doc/user/project/members/img/other_group_sees_shared_project.png b/doc/user/project/members/img/other_group_sees_shared_project.png
new file mode 100644
index 00000000000..e4c93a13abb
--- /dev/null
+++ b/doc/user/project/members/img/other_group_sees_shared_project.png
Binary files differ
diff --git a/doc/workflow/add-user/img/request_access_button.png b/doc/user/project/members/img/request_access_button.png
index 608baccb0ca..608baccb0ca 100644
--- a/doc/workflow/add-user/img/request_access_button.png
+++ b/doc/user/project/members/img/request_access_button.png
Binary files differ
diff --git a/doc/user/project/members/img/share_project_with_groups.png b/doc/user/project/members/img/share_project_with_groups.png
new file mode 100644
index 00000000000..0907438cb84
--- /dev/null
+++ b/doc/user/project/members/img/share_project_with_groups.png
Binary files differ
diff --git a/doc/user/project/members/img/share_project_with_groups_tab.png b/doc/user/project/members/img/share_project_with_groups_tab.png
new file mode 100644
index 00000000000..fc489aae003
--- /dev/null
+++ b/doc/user/project/members/img/share_project_with_groups_tab.png
Binary files differ
diff --git a/doc/workflow/add-user/img/withdraw_access_request_button.png b/doc/user/project/members/img/withdraw_access_request_button.png
index 6edd786b151..6edd786b151 100644
--- a/doc/workflow/add-user/img/withdraw_access_request_button.png
+++ b/doc/user/project/members/img/withdraw_access_request_button.png
Binary files differ
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
new file mode 100644
index 00000000000..b8dd96087f1
--- /dev/null
+++ b/doc/user/project/members/index.md
@@ -0,0 +1,116 @@
+# Project's members
+
+You can manage the groups and users and their access levels in all of your
+projects. You can also personalize the access level you give each user,
+per-project.
+
+You should have `master` or `owner` [permissions](../../permissions.md) to add
+or import a new user to your project.
+
+To view, edit, add, and remove project's members, go to your
+project's **Settings > Members**.
+
+---
+
+## Add a user
+
+Right next to **People**, start typing the name or username of the user you
+want to add.
+
+![Search for people](img/add_user_search_people.png)
+
+---
+
+Select the user and the [permission level](../../user/permissions.md)
+that you'd like to give the user. Note that you can select more than one user.
+
+![Give user permissions](img/add_user_give_permissions.png)
+
+---
+
+Once done, hit **Add users to project** and they will be immediately added to
+your project with the permissions you gave them above.
+
+![List members](img/add_user_list_members.png)
+
+---
+
+From there on, you can either remove an existing user or change their access
+level to the project.
+
+## Import users from another project
+
+You can import another project's users in your own project by hitting the
+**Import members** button on the upper right corner of the **Members** menu.
+
+In the dropdown menu, you can see only the projects you are Master on.
+
+![Import members from another project](img/add_user_import_members_from_another_project.png)
+
+---
+
+Select the one you want and hit **Import project members**. A flash message
+notifying you that the import was successful will appear, and the new members
+are now in the project's members list. Notice that the permissions that they
+had on the project you imported from are retained.
+
+![Members list of new members](img/add_user_imported_members.png)
+
+---
+
+## Invite people using their e-mail address
+
+If a user you want to give access to doesn't have an account on your GitLab
+instance, you can invite them just by typing their e-mail address in the
+user search field.
+
+![Invite user by mail](img/add_user_email_search.png)
+
+---
+
+As you can imagine, you can mix inviting multiple people and adding existing
+GitLab users to the project.
+
+![Invite user by mail ready to submit](img/add_user_email_ready.png)
+
+---
+
+Once done, hit **Add users to project** and watch that there is a new member
+with the e-mail address we used above. From there on, you can resend the
+invitation, change their access level or even delete them.
+
+![Invite user members list](img/add_user_email_accept.png)
+
+---
+
+Once the user accepts the invitation, they will be prompted to create a new
+GitLab account using the same e-mail address the invitation was sent to.
+
+## Request access to a project
+
+As a project owner you can enable or disable non members to request access to
+your project. Go to the project settings and click on **Allow users to request access**.
+
+As a user, you can request to be a member of a project. Go to the project you'd
+like to be a member of, and click the **Request Access** button on the right
+side of your screen.
+
+![Request access button](img/request_access_button.png)
+
+---
+
+Project owners & masters will be notified of your request and will be able to approve or
+decline it on the members page.
+
+![Manage access requests](img/access_requests_management.png)
+
+---
+
+If you change your mind before your request is approved, just click the
+**Withdraw Access Request** button.
+
+![Withdraw access request button](img/withdraw_access_request_button.png)
+
+## Share project with group
+
+Alternatively, you can [share a project with an entire group](share_project_with_groups.md) instead of adding users one by one.
diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md
new file mode 100644
index 00000000000..f5c748a03b3
--- /dev/null
+++ b/doc/user/project/members/share_project_with_groups.md
@@ -0,0 +1,43 @@
+# Share Projects with other Groups
+
+You can share projects with other [groups](../../group/index.md). This makes it
+possible to add a group of users to a project with a single action.
+
+## Groups as collections of users
+
+Groups are used primarily to [create collections of projects](../../group/index.md), but you can also
+take advantage of the fact that groups define collections of _users_, namely the group
+members.
+
+## Sharing a project with a group of users
+
+The primary mechanism to give a group of users, say 'Engineering', access to a project,
+say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project
+Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'?
+This is where the group sharing feature can be of use.
+
+To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the **Settings > Members** section.
+
+![share project with groups](img/share_project_with_groups.png)
+
+Then select the 'Share with group' tab by clicking it.
+
+Now you can add the 'Engineering' group with the maximum access level of your choice. Click 'Share' to share it.
+
+![share project with groups tab](img/share_project_with_groups_tab.png)
+
+After sharing 'Project Acme' with 'Engineering', the project will be listed on the group dashboard.
+
+!['Project Acme' is listed as a shared project for 'Engineering'](img/other_group_sees_shared_project.png)
+
+## Maximum access level
+
+In the example above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'.
+
+## Share project with group lock
+
+It is possible to prevent projects in a group from [sharing
+a project with another group](../members/share_project_with_groups.md).
+This allows for tighter control over project access.
+
+Learn more about [Share with group lock](../../group/index.html#share-with-group-lock).
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
new file mode 100644
index 00000000000..02a88d0112f
--- /dev/null
+++ 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_diff_file_navigation.png b/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png
new file mode 100644
index 00000000000..9b8aee47411
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/project_merge_requests_list_view.png b/doc/user/project/merge_requests/img/project_merge_requests_list_view.png
new file mode 100644
index 00000000000..702ec1a2949
--- /dev/null
+++ b/doc/user/project/merge_requests/img/project_merge_requests_list_view.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 9bdf2a998d3..26c6277d33a 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -56,6 +56,23 @@ B. Consider you're a web developer writing a webpage for your company's:
1. Once approved, your merge request is [squashed and merged](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html), and [deployed to staging with GitLab Pages](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) (Squash and Merge is available in GitLab Enterprise Edition Starter)
1. Your production team [cherry picks](#cherry-pick-changes) the merge commit into production
+## Merge requests per project
+
+View all the merge requests within a project by navigating to **Project > Merge Requests**.
+
+When you access your project's merge requests, GitLab will present them in a list,
+and you can use the tabs available to quickly filter by open and closed. You can also [search and filter the results](../../search/index.md#issues-and-merge-requests-per-project).
+
+![Project merge requests list view](img/project_merge_requests_list_view.png)
+
+## Merge requests per group
+
+View all the merge requests in a group (that is, all the merge requests across all projects in that
+group) by navigating to **Group > Merge Requests**. This view also has the open, merged, and closed
+merge request tabs, from which you can [search and filter the results](../../search/index.md#issues-and-merge-requests-per-group).
+
+![Group Issues list view](img/group_merge_requests_list_view.png)
+
## Authorization for merge requests
There are two main ways to have a merge request flow with GitLab:
@@ -120,6 +137,14 @@ have been marked as a **Work In Progress**.
[Learn more about settings a merge request as "Work In Progress".](work_in_progress_merge_requests.md)
+## Merge request diff file navigation
+
+The diff view has a persistent dropdown for file navigation. As you scroll through
+diffs with a large number of files and/or many changes in those files, you can
+easily jump to any changed file through the dropdown navigation.
+
+![Merge request diff file navigation](img/merge_request_diff_file_navigation.png)
+
## Ignore whitespace changes in Merge Request diff view
If you click the **Hide whitespace changes** button, you can see the diff
@@ -141,7 +166,6 @@ all your changes will be available to preview by anyone with the Review Apps lin
[Read more about Review Apps.](../../../ci/review_apps/index.md)
-
## Tips
Here are some tips that will help you be more efficient with merge requests in
@@ -230,4 +254,4 @@ git checkout origin/merge-requests/1
```
[protected branches]: ../protected_branches.md
-[ee]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition"
+[ee]: https://about.gitlab.com/gitlab-ee/ "GitLab Enterprise Edition" \ No newline at end of file
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index e853bfff444..4e93e680fd2 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -100,16 +100,21 @@ inside GitLab that make that possible.
It is possible to download the latest artifacts of a job via a well known URL
so you can use it for scripting purposes.
+>**Note:**
+The latest artifacts are considered as the artifacts created by jobs in the
+latest pipeline that succeeded for the specific ref.
+Artifacts for other pipelines can be accessed with direct access to them.
+
The structure of the URL to download the whole artifacts archive is the following:
```
-https://example.com/<namespace>/<project>/builds/artifacts/<ref>/download?job=<job_name>
+https://example.com/<namespace>/<project>/-/jobs/artifacts/<ref>/download?job=<job_name>
```
To download a single file from the artifacts use the following URL:
```
-https://example.com/<namespace>/<project>/builds/artifacts/<ref>/raw/<path_to_file>?job=<job_name>
+https://example.com/<namespace>/<project>/-/jobs/artifacts/<ref>/raw/<path_to_file>?job=<job_name>
```
For example, to download the latest artifacts of the job named `coverage` of
@@ -117,26 +122,26 @@ the `master` branch of the `gitlab-ce` project that belongs to the `gitlab-org`
namespace, the URL would be:
```
-https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/download?job=coverage
+https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/master/download?job=coverage
```
To download the file `coverage/index.html` from the same
artifacts use the following URL:
```
-https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/raw/coverage/index.html?job=coverage
+https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/master/raw/coverage/index.html?job=coverage
```
There is also a URL to browse the latest job artifacts:
```
-https://example.com/<namespace>/<project>/builds/artifacts/<ref>/browse?job=<job_name>
+https://example.com/<namespace>/<project>/-/jobs/artifacts/<ref>/browse?job=<job_name>
```
For example:
```
-https://gitlab.com/gitlab-org/gitlab-ce/builds/artifacts/master/browse?job=coverage
+https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/master/browse?job=coverage
```
The latest builds are also exposed in the UI in various places. Specifically,
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 3ff5a08d72c..dbc1305101f 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -66,10 +66,30 @@ in the pipelines settings page.
## Visibility of pipelines
-For public and internal projects, the pipelines page can be accessed by
-anyone and those logged in respectively. If you wish to hide it so that only
-the members of the project or group have access to it, uncheck the **Public
-pipelines** checkbox and save the changes.
+Access to pipelines and job details (including output of logs and artifacts)
+is checked against your current user access level and the **Public pipelines**
+project setting.
+
+If **Public pipelines** is enabled (default):
+
+- for **public** projects, anyone can view the pipelines and access the job details
+ (output logs and artifacts)
+- for **internal** projects, any logged in user can view the pipelines
+ and access the job details
+ (output logs and artifacts)
+- for **private** projects, any member (guest or higher) can view the pipelines
+ and access the job details
+ (output logs and artifacts)
+
+If **Public pipelines** is disabled:
+
+- for **public** projects, anyone can view the pipelines, but only members
+ (reporter or higher) can access the job details (output logs and artifacts)
+- for **internal** projects, any logged in user can view the pipelines,
+ but only members (reporter or higher) can access the job details (output logs
+ and artifacts)
+- for **private** projects, only members (reporter or higher)
+ can view the pipelines and access the job details (output logs and artifacts)
## Auto-cancel pending pipelines
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 0570d9f471f..0cbb0c878c2 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -115,6 +115,14 @@ Deleting a protected branch is only allowed via the web interface, not via Git.
This means that you can't accidentally delete a protected branch from your
command line or a Git client application.
+## Running pipelines on protected branches
+
+The permission to merge or push to protected branches is used to define if a user can
+run CI/CD pipelines and execute actions on jobs that are related to those branches.
+
+See [Security on protected branches](../../ci/pipelines.md#security-on-protected-branches)
+for details about the pipelines security model.
+
## Changelog
**9.2**
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index ce4dd4e99d5..6a5d2d40927 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -38,3 +38,4 @@ do.
| `/award :emoji:` | Toggle award for :emoji: |
| `/board_move ~column` | Move issue to column on the board |
| `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue |
+| `/move path/to/project` | Moves issue to another project |
diff --git a/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_paste_pub.png b/doc/user/project/repository/gpg_signed_commits/img/profile_settings_gpg_keys_paste_pub.png
index 8e26d98f1b0..8e26d98f1b0 100644
--- a/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_paste_pub.png
+++ b/doc/user/project/repository/gpg_signed_commits/img/profile_settings_gpg_keys_paste_pub.png
Binary files differ
diff --git a/doc/user/project/repository/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.png b/doc/user/project/repository/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.png
new file mode 100644
index 00000000000..5c14df36d73
--- /dev/null
+++ b/doc/user/project/repository/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.png
Binary files differ
diff --git a/doc/user/project/repository/gpg_signed_commits/img/project_signed_and_unsigned_commits.png b/doc/user/project/repository/gpg_signed_commits/img/project_signed_and_unsigned_commits.png
new file mode 100644
index 00000000000..088ecfa6d89
--- /dev/null
+++ b/doc/user/project/repository/gpg_signed_commits/img/project_signed_and_unsigned_commits.png
Binary files differ
diff --git a/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_unverified_signature.png b/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_unverified_signature.png
new file mode 100644
index 00000000000..4e3392406b1
--- /dev/null
+++ b/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_unverified_signature.png
Binary files differ
diff --git a/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_verified_signature.png b/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_verified_signature.png
new file mode 100644
index 00000000000..766970dee81
--- /dev/null
+++ b/doc/user/project/repository/gpg_signed_commits/img/project_signed_commit_verified_signature.png
Binary files differ
diff --git a/doc/user/project/repository/gpg_signed_commits/index.md b/doc/user/project/repository/gpg_signed_commits/index.md
new file mode 100644
index 00000000000..20aadb8f7ff
--- /dev/null
+++ b/doc/user/project/repository/gpg_signed_commits/index.md
@@ -0,0 +1,256 @@
+# Signing commits with GPG
+
+> [Introduced][ce-9546] in GitLab 9.5.
+
+GitLab can show whether a commit is verified or not when signed with a GPG key.
+All you need to do is upload the public GPG key in your profile settings.
+
+GPG verified tags are not supported yet.
+
+## Getting started with GPG
+
+Here are a few guides to get you started with GPG:
+
+- [Git Tools - Signing Your Work](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work)
+- [Managing OpenPGP Keys](https://riseup.net/en/security/message-security/openpgp/gpg-keys)
+- [OpenPGP Best Practices](https://riseup.net/en/security/message-security/openpgp/best-practices)
+- [Creating a new GPG key with subkeys](https://www.void.gr/kargig/blog/2013/12/02/creating-a-new-gpg-key-with-subkeys/) (advanced)
+
+## How GitLab handles GPG
+
+GitLab uses its own keyring to verify the GPG signature. It does not access any
+public key server.
+
+In order to have a commit verified on GitLab the corresponding public key needs
+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. The committer's email matches the verified email from the gpg key
+
+## Generating a GPG key
+
+>**Notes:**
+- If your Operating System has `gpg2` installed, replace `gpg` with `gpg2` in
+ the following commands.
+- If Git is using `gpg` and you get errors like `secret key not available` or
+ `gpg: signing failed: secret key not available`, run the following command to
+ change to `gpg2`:
+ ```
+ git config --global gpg.program gpg2
+ ```
+
+If you don't already have a GPG key, the following steps will help you get
+started:
+
+1. [Install GPG](https://www.gnupg.org/download/index.html) for your operating system
+1. Generate the private/public key pair with the following command:
+
+ ```sh
+ gpg --full-gen-key
+ ```
+
+ This will spawn a series of questions.
+
+1. The first question is which algorithm can be used. Select the kind you want
+ or press <kbd>Enter</kbd> to choose the default (RSA and RSA):
+
+ ```
+ Please select what kind of key you want:
+ (1) RSA and RSA (default)
+ (2) DSA and Elgamal
+ (3) DSA (sign only)
+ (4) RSA (sign only)
+ Your selection? 1
+ ```
+
+1. The next question is key length. We recommend to choose the highest value
+ which is `4096`:
+
+ ```
+ RSA keys may be between 1024 and 4096 bits long.
+ What keysize do you want? (2048) 4096
+ Requested keysize is 4096 bits
+ ```
+1. Next, you need to specify the validity period of your key. This is something
+ subjective, and you can use the default value which is to never expire:
+
+ ```
+ Please specify how long the key should be valid.
+ 0 = key does not expire
+ <n> = key expires in n days
+ <n>w = key expires in n weeks
+ <n>m = key expires in n months
+ <n>y = key expires in n years
+ Key is valid for? (0) 0
+ Key does not expire at all
+ ```
+
+1. Confirm that the answers you gave were correct by typing `y`:
+
+ ```
+ Is this correct? (y/N) y
+ ```
+
+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
+ (press <kbd>Enter</kbd> to skip):
+
+ ```
+ GnuPG needs to construct a user ID to identify your key.
+
+ Real name: Mr. Robot
+ Email address: mr@robot.sh
+ Comment:
+ You selected this USER-ID:
+ "Mr. Robot <mr@robot.sh>"
+
+ Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
+ ```
+
+1. Pick a strong password when asked and type it twice to confirm.
+1. Use the following command to list the private GPG key you just created:
+
+ ```
+ gpg --list-secret-keys 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`:
+
+ ```
+ sec rsa4096/0x30F2B65B9246B6CA 2017-08-18 [SC]
+ D5E4F29F3275DC0CDA8FFC8730F2B65B9246B6CA
+ uid [ultimate] Mr. Robot <mr@robot.sh>
+ ssb rsa4096/0xB7ABC0813E4028C0 2017-08-18 [E]
+ ```
+
+1. Export the public key of that ID (replace your key ID from the previous step):
+
+ ```
+ gpg --armor --export 0x30F2B65B9246B6CA
+ ```
+
+1. Finally, copy the public key and [add it in your profile settings](#adding-a-gpg-key-to-your-account)
+
+## Adding a GPG key to your account
+
+>**Note:**
+Once you add a key, you cannot edit it, only remove it. In case the paste
+didn't work, you'll have to remove the offending key and re-add it.
+
+You can add a GPG key in your profile's settings:
+
+1. On the upper right corner, click on your avatar and go to your **Settings**.
+
+ ![Settings dropdown](../../../profile/img/profile_settings_dropdown.png)
+
+1. Navigate to the **GPG keys** tab and paste your _public_ key in the 'Key'
+ box.
+
+ ![Paste GPG public key](img/profile_settings_gpg_keys_paste_pub.png)
+
+1. Finally, click on **Add key** to add it to GitLab. You will be able to see
+ its fingerprint, the corresponding email address and creation date.
+
+ ![GPG key single page](img/profile_settings_gpg_keys_single_key.png)
+
+## Associating your GPG key with Git
+
+After you have [created your GPG key](#generating-a-gpg-key) and [added it to
+your account](#adding-a-gpg-key-to-your-account), it's time to tell Git which
+key to use.
+
+1. Use the following command to list the private GPG key you just created:
+
+ ```
+ gpg --list-secret-keys 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`:
+
+ ```
+ sec rsa4096/0x30F2B65B9246B6CA 2017-08-18 [SC]
+ D5E4F29F3275DC0CDA8FFC8730F2B65B9246B6CA
+ uid [ultimate] Mr. Robot <mr@robot.sh>
+ ssb rsa4096/0xB7ABC0813E4028C0 2017-08-18 [E]
+ ```
+
+1. Tell Git to use that key to sign the commits:
+
+ ```
+ git config --global user.signingkey 0x30F2B65B9246B6CA
+ ```
+
+ Replace `0x30F2B65B9246B6CA` with your GPG key ID.
+
+## Signing commits
+
+After you have [created your GPG key](#generating-a-gpg-key) and [added it to
+your account](#adding-a-gpg-key-to-your-account), you can start signing your
+commits:
+
+1. Commit like you used to, the only difference is the addition of the `-S` flag:
+
+ ```
+ git commit -S -m "My commit msg"
+ ```
+
+1. Enter the passphrase of your GPG key when asked.
+1. Push to GitLab and check that your commits [are verified](#verifying-commits).
+
+If you don't want to type the `-S` flag every time you commit, you can tell Git
+to sign your commits automatically:
+
+```
+git config --global commit.gpgsign true
+```
+
+## Verifying commits
+
+1. Within a project or [merge request](../../merge_requests/index.md), navigate to
+ the **Commits** tab. Signed commits will show a badge containing either
+ "Verified" or "Unverified", depending on the verification status of the GPG
+ signature.
+
+ ![Signed and unsigned commits](img/project_signed_and_unsigned_commits.png)
+
+1. By clicking on the GPG badge, details of the signature are displayed.
+
+ ![Signed commit with verified signature](img/project_signed_commit_verified_signature.png)
+
+ ![Signed commit with verified signature](img/project_signed_commit_unverified_signature.png)
+
+## Revoking a GPG key
+
+Revoking a key **unverifies** already signed commits. Commits that were
+verified by using this key will change to an unverified state. Future commits
+will also stay unverified once you revoke this key. This action should be used
+in case your key has been compromised.
+
+To revoke a GPG key:
+
+1. On the upper right corner, click on your avatar and go to your **Settings**.
+1. Navigate to the **GPG keys** tab.
+1. Click on **Revoke** besides the GPG key you want to delete.
+
+## Removing a GPG key
+
+Removing a key **does not unverify** already signed commits. Commits that were
+verified by using this key will stay verified. Only unpushed commits will stay
+unverified once you remove this key. To unverify already signed commits, you need
+to [revoke the associated GPG key](#revoking-a-gpg-key) from your account.
+
+To remove a GPG key from your account:
+
+1. On the upper right corner, click on your avatar and go to your **Settings**.
+1. Navigate to the **GPG keys** tab.
+1. Click on the trash icon besides the GPG key you want to delete.
+
+[ce-9546]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9546
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index 4b2c435a120..235af83353d 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -20,7 +20,9 @@ documentation.
For security reasons, when using the command line, we strongly recommend
you to [connect with GitLab via SSH](../../../ssh/README.md).
-## Create and edit files
+## Files
+
+### Create and edit files
Host your codebase in GitLab repositories by pushing your files to GitLab.
You can either use the user interface (UI), or connect your local computer
@@ -47,6 +49,10 @@ it's easier to do so [via GitLab UI](web_editor.md):
To get started with the command line, please read through the
[command line basics documentation](../../../gitlab-basics/command-line-commands.md).
+### Find files
+
+Use GitLab's [file finder](../../../workflow/file_finder.md) to search for files in a repository.
+
## Branches
When you submit changes in a new branch, you create a new version
@@ -105,6 +111,8 @@ right from the UI.
- **Revert a commit:**
Easily [revert a commit](../merge_requests/revert_changes.md#reverting-a-commit)
from the UI to a selected branch.
+- **Sign a commit:**
+Use GPG to [sign your commits](gpg_signed_commits/index.md).
## Repository size
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 35960ade3d4..23b1c61cd16 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -9,6 +9,9 @@
> application settings (`/admin/application_settings`) under 'Import sources'.
> Ask your administrator if you don't see the **GitLab export** button when
> creating a new project.
+> - Starting with GitLab 10.0, administrators can disable the project export option
+> on the GitLab instance in application settings (`/admin/application_settings`)
+> under 'Visibility and Access Controls'.
> - You can find some useful raketasks if you are an administrator in the
> [import_export](../../../administration/raketasks/project_import_export.md)
> raketask.
@@ -25,17 +28,18 @@ with all their related data and be moved into a new GitLab instance.
## Version history
-| GitLab version | Import/Export version |
-| -------- | -------- |
-| 9.4.0 to current | 0.1.8 |
-| 9.2.0 | 0.1.7 |
-| 8.17.0 | 0.1.6 |
-| 8.13.0 | 0.1.5 |
-| 8.12.0 | 0.1.4 |
-| 8.10.3 | 0.1.3 |
-| 8.10.0 | 0.1.2 |
-| 8.9.5 | 0.1.1 |
-| 8.9.0 | 0.1.0 |
+| GitLab version | Import/Export version |
+| ---------------- | --------------------- |
+| 10.0 to current | 0.2.0 |
+| 9.4.0 | 0.1.8 |
+| 9.2.0 | 0.1.7 |
+| 8.17.0 | 0.1.6 |
+| 8.13.0 | 0.1.5 |
+| 8.12.0 | 0.1.4 |
+| 8.10.3 | 0.1.3 |
+| 8.10.0 | 0.1.2 |
+| 8.9.5 | 0.1.1 |
+| 8.9.0 | 0.1.0 |
> The table reflects what GitLab version we updated the Import/Export version at.
> For instance, 8.10.3 and 8.11 will have the same Import/Export version (0.1.3)
diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md
index e9ee1abc6c1..c0b8a87f038 100644
--- a/doc/user/project/wiki/index.md
+++ b/doc/user/project/wiki/index.md
@@ -4,7 +4,7 @@ A separate system for documentation called Wiki, is built right into each
GitLab project. It is enabled by default on all new projects and you can find
it under **Wiki** in your project.
-Wikis are very convenient if you don't want to keep you documentation in your
+Wikis are very convenient if you don't want to keep your documentation in your
repository, but you do want to keep it in the same project where your code
resides.
diff --git a/doc/user/reserved_names.md b/doc/user/reserved_names.md
new file mode 100644
index 00000000000..50ec99be48b
--- /dev/null
+++ b/doc/user/reserved_names.md
@@ -0,0 +1,109 @@
+# Reserved project and group names
+
+Not all project & group names are allowed because they would conflict with
+existing routes used by GitLab.
+
+For a list of words that are not allowed to be used as group or project names, see the
+[`path_regex.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `PROJECT_WILDCARD_ROUTES` and `GROUP_ROUTES` lists:
+- `TOP_LEVEL_ROUTES`: are names that are reserved as usernames or top level groups
+- `PROJECT_WILDCARD_ROUTES`: are names that are reserved for child groups or projects.
+- `GROUP_ROUTES`: are names that are reserved for all groups or projects.
+
+## Reserved project names
+
+It is currently not possible to create a project with the following names:
+
+- -
+- badges
+- blame
+- blob
+- builds
+- commits
+- create
+- create_dir
+- edit
+- environments/folders
+- files
+- find_file
+- gitlab-lfs/objects
+- info/lfs/objects
+- new
+- preview
+- raw
+- refs
+- tree
+- update
+- wikis
+
+## Reserved group names
+
+Currently the following names are reserved as top level groups:
+
+- 503.html
+- -
+- .well-known
+- 404.html
+- 422.html
+- 500.html
+- 502.html
+- abuse_reports
+- admin
+- api
+- apple-touch-icon-precomposed.png
+- apple-touch-icon.png
+- files
+- assets
+- autocomplete
+- ci
+- dashboard
+- deploy.html
+- explore
+- favicon.ico
+- groups
+- header_logo_dark.png
+- header_logo_light.png
+- health_check
+- help
+- import
+- invites
+- jwt
+- koding
+- notification_settings
+- oauth
+- profile
+- projects
+- public
+- robots.txt
+- s
+- search
+- sent_notifications
+- slash-command-logo.png
+- snippets
+- u
+- unicorn_test
+- unsubscribes
+- uploads
+- users
+
+These group names are unavailable as subgroup names:
+
+- -
+- activity
+- analytics
+- audit_events
+- avatar
+- edit
+- group_members
+- hooks
+- issues
+- labels
+- ldap
+- ldap_group_links
+- merge_requests
+- milestones
+- notification_setting
+- pipeline_quota
+- projects
+- subgroups
+
+[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/path_regex.rb
diff --git a/doc/user/search/img/group_issues_filter.png b/doc/user/search/img/group_issues_filter.png
new file mode 100644
index 00000000000..45eced79b99
--- /dev/null
+++ b/doc/user/search/img/group_issues_filter.png
Binary files differ
diff --git a/doc/user/search/img/issue_search_by_term.png b/doc/user/search/img/issue_search_by_term.png
new file mode 100644
index 00000000000..3cefa3adb8b
--- /dev/null
+++ b/doc/user/search/img/issue_search_by_term.png
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index 6d59dcc6c75..21e96d8b11c 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -27,7 +27,7 @@ on the search field on the top-right of your screen:
![shortcut to your issues and mrs](img/issues_mrs_shortcut.png)
-## Issues and merge requests per project
+### Issues and merge requests per project
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
@@ -40,6 +40,32 @@ The same process is valid for merge requests. Navigate to your project's **Merge
and click **Search or filter results...**. Merge requests can be filtered by author, assignee,
milestone, and label.
+### Searching for specific terms
+
+You can filter issues and merge requests by specific terms included in titles or descriptions.
+
+* Syntax
+ * Searches look for all the words in a query, in any order. E.g.: searching
+ issues for `display bug` will return all issues matching both those words, in any order.
+ * To find the exact term, use double quotes: `"display bug"`
+* Limitation
+ * For performance reasons, terms shorter than 3 chars are ignored. E.g.: searching
+ issues for `included in titles` is same as `included titles`
+
+![filter issues by specific terms](img/issue_search_by_term.png)
+
+### Issues and merge requests per group
+
+Similar to **Issues and merge requests per project**, you can also search for issues
+within a group. Navigate to a group's **Issues** tab and query search results in
+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
You can view recent searches by clicking on the little arrow-clock icon, which is to the left of the search input. Click the search entry to run that search again. This feature is available for issues and merge requests. Searches are stored locally in your browser.
diff --git a/doc/user/snippets.md b/doc/user/snippets.md
index 78861625f8a..2170b079f62 100644
--- a/doc/user/snippets.md
+++ b/doc/user/snippets.md
@@ -16,7 +16,7 @@ Comments on snippets was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/is
## Project snippets
-Project snippets are always related to a specific project - see [Project features](../workflow/project_features.md) for more information.
+Project snippets are always related to a specific project - see [Project's features](project/index.md#project-39-s-features) for more information.
## Personal snippets
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 925bbf76d49..673e08287a3 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -16,14 +16,13 @@
- [File finder](file_finder.md)
- [Labels](../user/project/labels.md)
- [Notification emails](notifications.md)
-- [Project Features](project_features.md)
+- [Projects](../user/project/index.md)
- [Project forking workflow](forking_workflow.md)
-- [Project users](add-user/add-user.md)
+- [Project users](../user/project/members/index.md)
- [Protected branches](../user/project/protected_branches.md)
- [Protected tags](../user/project/protected_tags.md)
- [Quick Actions](../user/project/quick_actions.md)
-- [Sharing a project with a group](share_with_group.md)
-- [Share projects with other groups](share_projects_with_other_groups.md)
+- [Sharing projects with groups](../user/project/members/share_project_with_groups.md)
- [Time tracking](time_tracking.md)
- [Web Editor](../user/project/repository/web_editor.md)
- [Releases](releases.md)
diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md
index e541111d7b3..35cc080d2b7 100644
--- a/doc/workflow/add-user/add-user.md
+++ b/doc/workflow/add-user/add-user.md
@@ -1,114 +1 @@
-# Project users
-
-You can manage the groups and users and their access levels in all of your
-projects. You can also personalize the access level you give each user,
-per-project.
-
-You should have `master` or `owner` permissions to add or import a new user
-to your project.
-
-The first step to add or import a user, go to your project and click on
-**Members** in the drop-down menu on the right side of your screen.
-
-![Members](img/add_user_members_menu.png)
-
----
-
-## Add a user
-
-Right next to **People**, start typing the name or username of the user you
-want to add.
-
-![Search for people](img/add_user_search_people.png)
-
----
-
-Select the user and the [permission level](../../user/permissions.md)
-that you'd like to give the user. Note that you can select more than one user.
-
-![Give user permissions](img/add_user_give_permissions.png)
-
----
-
-Once done, hit **Add users to project** and they will be immediately added to
-your project with the permissions you gave them above.
-
-![List members](img/add_user_list_members.png)
-
----
-
-From there on, you can either remove an existing user or change their access
-level to the project.
-
-## Import users from another project
-
-You can import another project's users in your own project by hitting the
-**Import members** button on the upper right corner of the **Members** menu.
-
-In the dropdown menu, you can see only the projects you are Master on.
-
-![Import members from another project](img/add_user_import_members_from_another_project.png)
-
----
-
-Select the one you want and hit **Import project members**. A flash message
-notifying you that the import was successful will appear, and the new members
-are now in the project's members list. Notice that the permissions that they
-had on the project you imported from are retained.
-
-![Members list of new members](img/add_user_imported_members.png)
-
----
-
-## Invite people using their e-mail address
-
-If a user you want to give access to doesn't have an account on your GitLab
-instance, you can invite them just by typing their e-mail address in the
-user search field.
-
-![Invite user by mail](img/add_user_email_search.png)
-
----
-
-As you can imagine, you can mix inviting multiple people and adding existing
-GitLab users to the project.
-
-![Invite user by mail ready to submit](img/add_user_email_ready.png)
-
----
-
-Once done, hit **Add users to project** and watch that there is a new member
-with the e-mail address we used above. From there on, you can resend the
-invitation, change their access level or even delete them.
-
-![Invite user members list](img/add_user_email_accept.png)
-
----
-
-Once the user accepts the invitation, they will be prompted to create a new
-GitLab account using the same e-mail address the invitation was sent to.
-
-## Request access to a project
-
-As a project owner you can enable or disable non members to request access to
-your project. Go to the project settings and click on **Allow users to request access**.
-
-As a user, you can request to be a member of a project. Go to the project you'd
-like to be a member of, and click the **Request Access** button on the right
-side of your screen.
-
-![Request access button](img/request_access_button.png)
-
----
-
-Project owners & masters will be notified of your request and will be able to approve or
-decline it on the members page.
-
-![Manage access requests](img/access_requests_management.png)
-
----
-
-If you change your mind before your request is approved, just click the
-**Withdraw Access Request** button.
-
-![Withdraw access request button](img/withdraw_access_request_button.png)
+This document was moved to [../../user/project/members/index.md](../../user/project/members/index.md)
diff --git a/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys.png b/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys.png
deleted file mode 100644
index e525083918b..00000000000
--- a/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.png b/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.png
deleted file mode 100644
index f715c46adc3..00000000000
--- a/doc/workflow/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/gpg_signed_commits/img/project_signed_and_unsigned_commits.png b/doc/workflow/gpg_signed_commits/img/project_signed_and_unsigned_commits.png
deleted file mode 100644
index 16ec2d031ae..00000000000
--- a/doc/workflow/gpg_signed_commits/img/project_signed_and_unsigned_commits.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/gpg_signed_commits/img/project_signed_commit_unverified_signature.png b/doc/workflow/gpg_signed_commits/img/project_signed_commit_unverified_signature.png
deleted file mode 100644
index 22565cf7c7e..00000000000
--- a/doc/workflow/gpg_signed_commits/img/project_signed_commit_unverified_signature.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/gpg_signed_commits/img/project_signed_commit_verified_signature.png b/doc/workflow/gpg_signed_commits/img/project_signed_commit_verified_signature.png
deleted file mode 100644
index 1778b2ddf2b..00000000000
--- a/doc/workflow/gpg_signed_commits/img/project_signed_commit_verified_signature.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/gpg_signed_commits/index.md b/doc/workflow/gpg_signed_commits/index.md
deleted file mode 100644
index 7d5762d2b9d..00000000000
--- a/doc/workflow/gpg_signed_commits/index.md
+++ /dev/null
@@ -1,84 +0,0 @@
-# Signing commits with GPG
-
-## Getting started
-
-- [Git Tools - Signing Your Work](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work)
-- [Git Tools - Signing Your Work: GPG introduction](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work#_gpg_introduction)
-- [Git Tools - Signing Your Work: Signing commits](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work#_signing_commits)
-
-## How GitLab handles GPG
-
-GitLab uses its own keyring to verify the GPG signature. It does not access any
-public key server.
-
-In order to have a commit verified on GitLab the corresponding public key needs
-to be uploaded to GitLab.
-
-For a signature to be verified two prerequisites need to be met:
-
-1. The public key needs to be added to GitLab
-1. One of the emails in the GPG key matches your **primary** email
-
-## Add a GPG key
-
-1. On the upper right corner, click on your avatar and go to your **Settings**.
-
- ![Settings dropdown](../../gitlab-basics/img/profile_settings.png)
-
-1. Navigate to the **GPG keys** tab.
-
- ![GPG Keys](img/profile_settings_gpg_keys.png)
-
-1. Paste your **public** key in the 'Key' box.
-
- ![Paste GPG public key](img/profile_settings_gpg_keys_paste_pub.png)
-
-1. Finally, click on **Add key** to add it to GitLab. You will be able to see
- its fingerprint, the corresponding email address and creation date.
-
- ![GPG key single page](img/profile_settings_gpg_keys_single_key.png)
-
->**Note:**
-Once you add a key, you cannot edit it, only remove it. In case the paste
-didn't work, you will have to remove the offending key and re-add it.
-
-## Remove a GPG key
-
-1. On the upper right corner, click on your avatar and go to your **Settings**.
-
-1. Navigate to the **GPG keys** tab.
-
-1. Click on the trash icon besides the GPG key you want to delete.
-
->**Note:**
-Removing a key **does not unverify** already signed commits. Commits that were
-verified by using this key will stay verified. Only unpushed commits will stay
-unverified once you remove this key.
-
-## Revoke a GPG key
-
-1. On the upper right corner, click on your avatar and go to your **Settings**.
-
-1. Navigate to the **GPG keys** tab.
-
-1. Click on **Revoke** besides the GPG key you want to delete.
-
->**Note:**
-Revoking a key **unverifies** already signed commits. Commits that were
-verified by using this key will change to an unverified state. Future commits
-will also stay unverified once you revoke this key. This action should be used
-in case your key has been compromised.
-
-## Verifying commits
-
-1. Within a project navigate to the **Commits** tag. Signed commits will show a
- badge containing either "Verified" or "Unverified", depending on the
- verification status of the GPG signature.
-
- ![Signed and unsigned commits](img/project_signed_and_unsigned_commits.png)
-
-1. By clicking on the GPG badge details of the signature are displayed.
-
- ![Signed commit with verified signature](img/project_signed_commit_verified_signature.png)
-
- ![Signed commit with verified signature](img/project_signed_commit_unverified_signature.png)
diff --git a/doc/workflow/groups/other_group_sees_shared_project.png b/doc/workflow/groups/other_group_sees_shared_project.png
deleted file mode 100644
index 67af27043eb..00000000000
--- a/doc/workflow/groups/other_group_sees_shared_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/groups/share_project_with_groups.png b/doc/workflow/groups/share_project_with_groups.png
deleted file mode 100644
index 3cb4796f9f7..00000000000
--- a/doc/workflow/groups/share_project_with_groups.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md
index 2d91bee0e94..f753708ad89 100644
--- a/doc/workflow/importing/README.md
+++ b/doc/workflow/importing/README.md
@@ -1,17 +1 @@
-# Migrating projects to a GitLab instance
-
-1. [Bitbucket](import_projects_from_bitbucket.md)
-1. [GitHub](import_projects_from_github.md)
-1. [GitLab.com](import_projects_from_gitlab_com.md)
-1. [FogBugz](import_projects_from_fogbugz.md)
-1. [Gitea](import_projects_from_gitea.md)
-1. [SVN](migrating_from_svn.md)
-
-In addition to the specific migration documentation above, you can import any
-Git repository via HTTP from the New Project page. Be aware that if the
-repository is too large the import can timeout.
-
-### Migrating from self-hosted GitLab to GitLab.com
-
-You can copy your repos by changing the remote and pushing to the new server;
-but issues and merge requests can't be imported.
+This document was moved to a [new location](../../user/project/import/index.md).
diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md
index f3c636ed1d5..248c3990372 100644
--- a/doc/workflow/importing/import_projects_from_bitbucket.md
+++ b/doc/workflow/importing/import_projects_from_bitbucket.md
@@ -1,62 +1 @@
-# Import your project from Bitbucket to GitLab
-
-Import your projects from Bitbucket to GitLab with minimal effort.
-
-## Overview
-
->**Note:**
-The [Bitbucket integration][bb-import] must be first enabled in order to be
-able to import your projects from Bitbucket. Ask your GitLab administrator
-to enable this if not already.
-
-- At its current state, the Bitbucket importer can import:
- - the repository description (GitLab 7.7+)
- - the Git repository data (GitLab 7.7+)
- - the issues (GitLab 7.7+)
- - the issue comments (GitLab 8.15+)
- - the pull requests (GitLab 8.4+)
- - the pull request comments (GitLab 8.15+)
- - the milestones (GitLab 8.15+)
- - the wiki (GitLab 8.15+)
-- References to pull requests and issues are preserved (GitLab 8.7+)
-- Repository public access is retained. If a repository is private in Bitbucket
- it will be created as private in GitLab as well.
-
-
-## How it works
-
-When issues/pull requests are being imported, the Bitbucket importer tries to find
-the Bitbucket author/assignee in GitLab's database using the Bitbucket ID. For this
-to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
-and **associated their Bitbucket account**. If the user is not
-found in GitLab's database, the project creator (most of the times the current
-user that started the import process) is set as the author, but a reference on
-the issue about the original Bitbucket author is kept.
-
-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.
-
-## Importing your Bitbucket repositories
-
-1. Sign in to GitLab and go to your dashboard.
-1. Click on **New project**.
-
- ![New project in GitLab](img/bitbucket_import_new_project.png)
-
-1. Click on the "Bitbucket" button
-
- ![Bitbucket](img/import_projects_from_new_project_page.png)
-
-1. Grant GitLab access to your Bitbucket account
-
- ![Grant access](img/bitbucket_import_grant_access.png)
-
-1. Click on the projects that you'd like to import or **Import all projects**.
- You can also select the namespace under which each project will be
- imported.
-
- ![Import projects](img/bitbucket_import_select_project.png)
-
-[bb-import]: ../../integration/bitbucket.md
-[social sign-in]: ../../user/profile/account/social_sign_in.md
+This document was moved to a [new location](../../user/project/import/bitbucket.md).
diff --git a/doc/workflow/importing/import_projects_from_fogbugz.md b/doc/workflow/importing/import_projects_from_fogbugz.md
index 71af0f9ea44..050746e2b4d 100644
--- a/doc/workflow/importing/import_projects_from_fogbugz.md
+++ b/doc/workflow/importing/import_projects_from_fogbugz.md
@@ -1,29 +1 @@
-# Import your project from FogBugz to GitLab
-
-It only takes a few simple steps to import your project from FogBugz.
-The importer will import all of your cases and comments with original case
-numbers and timestamps. You will also have the opportunity to map FogBugz
-users to GitLab users.
-
-* From your GitLab dashboard click 'New project'
-
-* Click on the 'FogBugz' button
-
-![FogBugz](fogbugz_importer/fogbugz_import_select_fogbogz.png)
-
-* Enter your FogBugz URL, email address, and password.
-
-![Login](fogbugz_importer/fogbugz_import_login.png)
-
-* Create mapping from FogBugz users to GitLab users.
-
-![User Map](fogbugz_importer/fogbugz_import_user_map.png)
-
-* Select the projects you wish to import by clicking the Import buttons
-
-![Import Project](fogbugz_importer/fogbugz_import_select_project.png)
-
-* Once the import has finished click the link to take you to the project
-dashboard. Follow the directions to push your existing repository.
-
-![Finished](fogbugz_importer/fogbugz_import_finished.png)
+This document was moved to a [new location](../../user/project/import/fogbugz.md).
diff --git a/doc/workflow/importing/import_projects_from_gitea.md b/doc/workflow/importing/import_projects_from_gitea.md
index f5746a0fb31..cb90c490b0f 100644
--- a/doc/workflow/importing/import_projects_from_gitea.md
+++ b/doc/workflow/importing/import_projects_from_gitea.md
@@ -1,77 +1 @@
-# Import your project from Gitea to GitLab
-
-Import your projects from Gitea to GitLab with minimal effort.
-
-## Overview
-
->**Note:**
-This requires Gitea `v1.0.0` or newer.
-
-- At its current state, Gitea importer can import:
- - the repository description (GitLab 8.15+)
- - the Git repository data (GitLab 8.15+)
- - the issues (GitLab 8.15+)
- - the pull requests (GitLab 8.15+)
- - the milestones (GitLab 8.15+)
- - the labels (GitLab 8.15+)
-- Repository public access is retained. If a repository is private in Gitea
- it will be created as private in GitLab as well.
-
-## How it works
-
-Since Gitea is currently not an OAuth provider, author/assignee cannot be mapped
-to users in your GitLab's instance. This means that the project creator (most of
-the times the current user that started the import process) is set as the author,
-but a reference on the issue about the original Gitea author is kept.
-
-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.
-
-## Importing your Gitea repositories
-
-The importer page is visible when you create a new project.
-
-![New project page on GitLab](img/import_projects_from_new_project_page.png)
-
-Click on the **Gitea** link and the import authorization process will start.
-
-![New Gitea project import](img/import_projects_from_gitea_new_import.png)
-
-### Authorize access to your repositories using a personal access token
-
-With this method, you will perform a one-off authorization with Gitea to grant
-GitLab access your repositories:
-
-1. Go to <https://you-gitea-instance/user/settings/applications> (replace
- `you-gitea-instance` with the host of your Gitea instance).
-1. Click **Generate New Token**.
-1. Enter a token description.
-1. Click **Generate Token**.
-1. Copy the token hash.
-1. Go back to GitLab and provide the token to the Gitea importer.
-1. Hit the **List Your Gitea Repositories** button and wait while GitLab reads
- your repositories' information. Once done, you'll be taken to the importer
- page to select the repositories to import.
-
-### Select which repositories to import
-
-After you've authorized access to your Gitea repositories, you will be
-redirected to the Gitea importer page.
-
-From there, you can see the import statuses of your Gitea repositories.
-
-- Those that are being imported will show a _started_ status,
-- those already successfully imported will be green with a _done_ status,
-- whereas those that are not yet imported will have an **Import** button on the
- right side of the table.
-
-If you want, you can import all your Gitea projects in one go by hitting
-**Import all projects** in the upper left corner.
-
-![Gitea importer page](img/import_projects_from_github_importer.png)
-
----
-
-You can also choose a different name for the project and a different namespace,
-if you have the privileges to do so.
+This document was moved to a [new location](../../user/project/import/gitea.md).
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index 8ed1d98d05b..13639feaa04 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -1,122 +1 @@
-# Import your project from GitHub to GitLab
-
-Import your projects from GitHub to GitLab with minimal effort.
-
-## Overview
-
->**Note:**
-If you are an administrator you can enable the [GitHub integration][gh-import]
-in your GitLab instance sitewide. This configuration is optional, users will
-still be able to import their GitHub repositories with a
-[personal access token][gh-token].
-
->**Note:**
-Administrators of a GitLab instance (Community or Enterprise Edition) can also
-use the [GitHub rake task][gh-rake] to import projects from GitHub without the
-constrains of a Sidekiq worker.
-
-- At its current state, GitHub importer can import:
- - the repository description (GitLab 7.7+)
- - the Git repository data (GitLab 7.7+)
- - the issues (GitLab 7.7+)
- - the pull requests (GitLab 8.4+)
- - the wiki pages (GitLab 8.4+)
- - the milestones (GitLab 8.7+)
- - the labels (GitLab 8.7+)
- - the release note descriptions (GitLab 8.12+)
-- References to pull requests and issues are preserved (GitLab 8.7+)
-- Repository public access is retained. If a repository is private in GitHub
- it will be created as private in GitLab as well.
-
-## How it works
-
-When issues/pull requests are being imported, the GitHub importer tries to find
-the GitHub author/assignee in GitLab's database using the GitHub ID. For this
-to work, the GitHub author/assignee should have signed in beforehand in GitLab
-and **associated their GitHub account**. If the user is not
-found in GitLab's database, the project creator (most of the times the current
-user that started the import process) is set as the author, but a reference on
-the issue about the original GitHub author is kept.
-
-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.
-
-## Importing your GitHub repositories
-
-The importer page is visible when you create a new project.
-
-![New project page on GitLab](img/import_projects_from_new_project_page.png)
-
-Click on the **GitHub** link and the import authorization process will start.
-There are two ways to authorize access to your GitHub repositories:
-
-1. [Using the GitHub integration][gh-integration] (if it's enabled by your
- GitLab administrator). This is the preferred way as it's possible to
- preserve the GitHub authors/assignees. Read more in the [How it works](#how-it-works)
- section.
-1. [Using a personal access token][gh-token] provided by GitHub.
-
-![Select authentication method](img/import_projects_from_github_select_auth_method.png)
-
-### Authorize access to your repositories using the GitHub integration
-
-If the [GitHub integration][gh-import] is enabled by your GitLab administrator,
-you can use it instead of the personal access token.
-
-1. First you may want to connect your GitHub account to GitLab in order for
- the username mapping to be correct.
-1. Once you connect GitHub, click the **List your GitHub repositories** button
- and you will be redirected to GitHub for permission to access your projects.
-1. After accepting, you'll be automatically redirected to the importer.
-
-You can now go on and [select which repositories to import](#select-which-repositories-to-import).
-
-### Authorize access to your repositories using a personal access token
-
->**Note:**
-For a proper author/assignee mapping for issues and pull requests, the
-[GitHub integration][gh-integration] should be used instead of the
-[personal access token][gh-token]. If the GitHub integration is enabled by your
-GitLab administrator, it should be the preferred method to import your repositories.
-Read more in the [How it works](#how-it-works) section.
-
-If you are not using the GitHub integration, you can still perform a one-off
-authorization with GitHub to grant GitLab access your repositories:
-
-1. Go to <https://github.com/settings/tokens/new>.
-1. Enter a token description.
-1. Check the `repo` scope.
-1. Click **Generate token**.
-1. Copy the token hash.
-1. Go back to GitLab and provide the token to the GitHub importer.
-1. Hit the **List Your GitHub Repositories** button and wait while GitLab reads
- your repositories' information. Once done, you'll be taken to the importer
- page to select the repositories to import.
-
-### Select which repositories to import
-
-After you've authorized access to your GitHub repositories, you will be
-redirected to the GitHub importer page.
-
-From there, you can see the import statuses of your GitHub repositories.
-
-- Those that are being imported will show a _started_ status,
-- those already successfully imported will be green with a _done_ status,
-- whereas those that are not yet imported will have an **Import** button on the
- right side of the table.
-
-If you want, you can import all your GitHub projects in one go by hitting
-**Import all projects** in the upper left corner.
-
-![GitHub importer page](img/import_projects_from_github_importer.png)
-
----
-
-You can also choose a different name for the project and a different namespace,
-if you have the privileges to do so.
-
-[gh-import]: ../../integration/github.md "GitHub integration"
-[gh-rake]: ../../administration/raketasks/github_import.md "GitHub rake task"
-[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration
-[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token
+This document was moved to a [new location](../../user/project/import/github.md).
diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md
index b27125a44de..df4c180919a 100644
--- a/doc/workflow/importing/import_projects_from_gitlab_com.md
+++ b/doc/workflow/importing/import_projects_from_gitlab_com.md
@@ -1,21 +1 @@
-# Project importing from GitLab.com to your private GitLab instance
-
-You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if
-GitLab support is enabled on your GitLab instance.
-You can read more about GitLab support [here](http://docs.gitlab.com/ce/integration/gitlab.html)
-To get to the importer page you need to go to "New project" page.
-
->**Note:**
-If you are interested in importing Wiki and Merge Request data to your new instance, you'll need to follow the instructions for [project export](../../user/project/settings/import_export.md)
-
-![New project page](gitlab_importer/new_project_page.png)
-
-Click on the "Import projects from GitLab.com" link and you will be redirected to GitLab.com
-for permission to access your projects. After accepting, you'll be automatically redirected to the importer.
-
-
-![Importer page](gitlab_importer/importer.png)
-
-
-To import a project, you can simple click "Import". The importer will import your repository and issues.
-Once the importer is done, a new GitLab project will be created with your imported data. \ No newline at end of file
+This document was moved to a [new location](../../user/project/import/gitlab_com.md).
diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index 7a3628a39d7..81df3fbcdbb 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -1,183 +1 @@
-# Migrating from SVN to GitLab
-
-Subversion (SVN) is a central version control system (VCS) while
-Git is a distributed version control system. There are some major differences
-between the two, for more information consult your favorite search engine.
-
-## Overview
-
-There are two approaches to SVN to Git migration:
-
-1. [Git/SVN Mirror](#smooth-migration-with-a-gitsvn-mirror-using-subgit) which:
- - Makes the GitLab repository to mirror the SVN project.
- - Git and SVN repositories are kept in sync; you can use either one.
- - Smoothens the migration process and allows to manage migration risks.
-
-1. [Cut over migration](#cut-over-migration-with-svn2git) which:
- - Translates and imports the existing data and history from SVN to Git.
- - Is a fire and forget approach, good for smaller teams.
-
-## Smooth migration with a Git/SVN mirror using SubGit
-
-[SubGit](https://subgit.com) is a tool for a smooth, stress-free SVN to Git
-migration. It creates a writable Git mirror of a local or remote Subversion
-repository and that way you can use both Subversion and Git as long as you like.
-It requires access to your GitLab server as it talks with the Git repositories
-directly in a filesystem level.
-
-### SubGit prerequisites
-
-1. Install Oracle JRE 1.8 or newer. On Debian-based Linux distributions you can
- follow [this article](http://www.webupd8.org/2012/09/install-oracle-java-8-in-ubuntu-via-ppa.html).
-1. Download SubGit from https://subgit.com/download/.
-1. Unpack the downloaded SubGit zip archive to the `/opt` directory. The `subgit`
- command will be available at `/opt/subgit-VERSION/bin/subgit`.
-
-### SubGit configuration
-
-The first step to mirror you SVN repository in GitLab is to create a new empty
-project which will be used as a mirror. For Omnibus installations the path to
-the repository will be located at
-`/var/opt/gitlab/git-data/repositories/USER/REPO.git` by default. For
-installations from source, the default repository directory will be
-`/home/git/repositories/USER/REPO.git`. For convenience, assign this path to a
-variable:
-
-```
-GIT_REPO_PATH=/var/opt/gitlab/git-data/repositories/USER/REPOS.git
-```
-
-SubGit will keep this repository in sync with a remote SVN project. For
-convenience, assign your remote SVN project URL to a variable:
-
-```
-SVN_PROJECT_URL=http://svn.company.com/repos/project
-```
-
-Next you need to run SubGit to set up a Git/SVN mirror. Make sure the following
-`subgit` command is ran on behalf of the same user that keeps ownership of
-GitLab Git repositories (by default `git`):
-
-```
-subgit configure --layout auto $SVN_PROJECT_URL $GIT_REPO_PATH
-```
-
-Adjust authors and branches mappings, if necessary. Open with your favorite
-text editor:
-
-```
-edit $GIT_REPO_PATH/subgit/authors.txt
-edit $GIT_REPO_PATH/subgit/config
-```
-
-For more information regarding the SubGit configuration options, refer to
-[SubGit's documentation](https://subgit.com/documentation.html) website.
-
-### Initial translation
-
-Now that SubGit has configured the Git/SVN repos, run `subgit` to perform the
-initial translation of existing SVN revisions into the Git repository:
-
-```
-subgit install $GIT_REPO_PATH
-```
-
-After the initial translation is completed, the Git repository and the SVN
-project will be kept in sync by `subgit` - new Git commits will be translated to
-SVN revisions and new SVN revisions will be translated to Git commits. Mirror
-works transparently and does not require any special commands.
-
-If you would prefer to perform one-time cut over migration with `subgit`, use
-the `import` command instead of `install`:
-
-```
-subgit import $GIT_REPO_PATH
-```
-
-### SubGit licensing
-
-Running SubGit in a mirror mode requires a
-[registration](https://subgit.com/pricing.html). Registration is free for open
-source, academic and startup projects.
-
-We're currently working on deeper GitLab/SubGit integration. You may track our
-progress at [this issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/990).
-
-### SubGit support
-
-For any questions related to SVN to GitLab migration with SubGit, you can
-contact the SubGit team directly at [support@subgit.com](mailto:support@subgit.com).
-
-## Cut over migration with svn2git
-
-If you are currently using an SVN repository, you can migrate the repository
-to Git and GitLab. We recommend a hard cut over - run the migration command once
-and then have all developers start using the new GitLab repository immediately.
-Otherwise, it's hard to keep changing in sync in both directions. The conversion
-process should be run on a local workstation.
-
-Install `svn2git`. On all systems you can install as a Ruby gem if you already
-have Ruby and Git installed.
-
-```bash
-sudo gem install svn2git
-```
-
-On Debian-based Linux distributions you can install the native packages:
-
-```bash
-sudo apt-get install git-core git-svn ruby
-```
-
-Optionally, prepare an authors file so `svn2git` can map SVN authors to Git authors.
-If you choose not to create the authors file then commits will not be attributed
-to the correct GitLab user. Some users may not consider this a big issue while
-others will want to ensure they complete this step. If you choose to map authors
-you will be required to map every author that is present on changes in the SVN
-repository. If you don't, the conversion will fail and you will have to update
-the author file accordingly. The following command will search through the
-repository and output a list of authors.
-
-```bash
-svn log --quiet | grep -E "r[0-9]+ \| .+ \|" | cut -d'|' -f2 | sed 's/ //g' | sort | uniq
-```
-
-Use the output from the last command to construct the authors file.
-Create a file called `authors.txt` and add one mapping per line.
-
-```
-janedoe = Jane Doe <janedoe@example.com>
-johndoe = John Doe <johndoe@example.com>
-```
-
-If your SVN repository is in the standard format (trunk, branches, tags,
-not nested) the conversion is simple. For a non-standard repository see
-[svn2git documentation](https://github.com/nirvdrum/svn2git). The following
-command will checkout the repository and do the conversion in the current
-working directory. Be sure to create a new directory for each repository before
-running the `svn2git` command. The conversion process will take some time.
-
-```bash
-svn2git https://svn.example.com/path/to/repo --authors /path/to/authors.txt
-```
-
-If your SVN repository requires a username and password add the
-`--username <username>` and `--password <password` flags to the above command.
-`svn2git` also supports excluding certain file paths, branches, tags, etc. See
-[svn2git documentation](https://github.com/nirvdrum/svn2git) or run
-`svn2git --help` for full documentation on all of the available options.
-
-Create a new GitLab project, where you will eventually push your converted code.
-Copy the SSH or HTTP(S) repository URL from the project page. Add the GitLab
-repository as a Git remote and push all the changes. This will push all commits,
-branches and tags.
-
-```bash
-git remote add origin git@gitlab.com:<group>/<project>.git
-git push --all origin
-git push --tags origin
-```
-
-## Contribute to this guide
-We welcome all contributions that would expand this guide with instructions on
-how to migrate from SVN and other version control systems.
+This document was moved to a [new location](../../user/project/import/svn.md).
diff --git a/doc/workflow/project_features.md b/doc/workflow/project_features.md
index 3f5de2bd4b1..feb88712f5a 100644
--- a/doc/workflow/project_features.md
+++ b/doc/workflow/project_features.md
@@ -1,45 +1 @@
-# Project features
-
-When in a Project -> Settings, you will find Features on the bottom of the page that you can toggle.
-
-Below you will find a more elaborate explanation of each of these.
-
-## Issues
-
-Issues is a really powerful, but lightweight issue tracking system.
-
-You can make tickets, assign them to people, file them under milestones, order them with labels and have discussion in them.
-
-They integrate deeply into GitLab and are easily referenced from anywhere by using `#` and the issue number.
-
-## Merge Requests
-
-Using a merge request, you can review and discuss code before it is merged in the branch of your code.
-
-As with issues, it can be assigned; people, issues, etc. can be referenced; milestones attached.
-
-We see it as an integral part of working together on code and couldn't work without it.
-
-## Wiki
-
-This is a separate system for documentation, built right into GitLab.
-
-It is source controlled and is very convenient if you don't want to keep you documentation in your source code, but you do want to keep it in your GitLab project.
-
-[Read more about Wikis.](../user/project/wiki/index.md)
-
-## Snippets
-
-Snippets are little bits of code or text.
-
-This is a nice place to put code or text that is used semi-regularly within the project, but does not belong in source control.
-
-For example, a specific config file that is used by the team that is only valid for the people that work on the code.
-
-## Git LFS
-
->**Note:** Project-specific LFS setting was added on 8.12 and is available only to admins.
-
-Git Large File Storage allows you to easily manage large binary files with Git.
-With this setting admins can better control which projects are allowed to use
-LFS.
+This document was moved to [../user/project/index.md](../user/project/index.md)
diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md
index 40d756bc199..2eb4d24958a 100644
--- a/doc/workflow/share_projects_with_other_groups.md
+++ b/doc/workflow/share_projects_with_other_groups.md
@@ -1,32 +1 @@
-# Share Projects with other Groups
-
-You can share projects with other groups. This makes it possible to add a group of users
-to a project with a single action.
-
-## Groups as collections of users
-
-Groups are used primarily to [create collections of projects](../user/group/index.md), but you can also
-take advantage of the fact that groups define collections of _users_, namely the group
-members.
-
-## Sharing a project with a group of users
-
-The primary mechanism to give a group of users, say 'Engineering', access to a project,
-say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project
-Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'?
-This is where the group sharing feature can be of use.
-
-To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
-
-![The 'Groups' section in the project settings screen](groups/share_project_with_groups.png)
-
-Now you can add the 'Engineering' group with the maximum access level of your choice.
-After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
-
-!['Project Acme' is listed as a shared project for 'Engineering'](groups/other_group_sees_shared_project.png)
-
-## Maximum access level
-
-!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](groups/max_access_level.png)
-
-In the screenshot above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'.
+This document was moved to [../user/project/members/share_project_with_groups.md](../user/project/members/share_project_with_groups.md)
diff --git a/doc/workflow/share_with_group.md b/doc/workflow/share_with_group.md
index 3b7690973cb..2eb4d24958a 100644
--- a/doc/workflow/share_with_group.md
+++ b/doc/workflow/share_with_group.md
@@ -1,13 +1 @@
-# Sharing a project with a group
-
-If you want to share a single project in a group with another group,
-you can do so easily. By setting the permission you can quickly
-give a select group of users access to a project in a restricted manner.
-
-In a project go to the project settings -> groups.
-
-Now you can select a group that you want to share this project with and with
-which maximum access level. Users in that group are able to access this project
-with their set group access level, up to the maximum level that you've set.
-
-![Share a project with a group](share_with_group.png)
+This document was moved to [../user/project/members/share_project_with_groups.md](../user/project/members/share_project_with_groups.md)
diff --git a/docker/README.md b/docker/README.md
index f9e12c5733b..61b41d2f109 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -1,7 +1,3 @@
# GitLab Docker images
-* The official GitLab Community Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ce/).
-* The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ee/).
-* The complete usage guide can be found in [Using GitLab Docker images](https://docs.gitlab.com/omnibus/docker/)
-* The Dockerfile used for building public images is in [Omnibus Repository](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker)
-* Check the guide for [creating Omnibus-based Docker Image](https://docs.gitlab.com/omnibus/build/README.html#build-docker-image)
+This content has been moved to [our documentation site](https://docs.gitlab.com/ce/install/docker.html).
diff --git a/features/project/commits/user_lookup.feature b/features/project/commits/user_lookup.feature
deleted file mode 100644
index c18f4e070f3..00000000000
--- a/features/project/commits/user_lookup.feature
+++ /dev/null
@@ -1,16 +0,0 @@
-@project_commits
-Feature: Project Commits User Lookup
- Background:
- Given I sign in as a user
- And I own a project
- And I visit my project's commits page
-
- Scenario: I browse commit from list
- Given I have user with primary email
- When I click on commit link
- Then I see author based on primary email
-
- Scenario: I browse another commit from list
- Given I have user with secondary email
- When I click on another commit link
- Then I see author based on secondary email
diff --git a/features/project/source/search_code.feature b/features/project/source/search_code.feature
deleted file mode 100644
index 4f9dcea249f..00000000000
--- a/features/project/source/search_code.feature
+++ /dev/null
@@ -1,15 +0,0 @@
-Feature: Project Source Search Code
- Background:
- Given I sign in as a user
-
- Scenario: Search for term "coffee"
- Given I own project "Shop"
- And I visit project source page
- When I search for term "coffee"
- Then I should see files from repository containing "coffee"
-
- Scenario: Search on empty project
- Given I own an empty project
- And I visit my project's home page
- When I search for term "coffee"
- Then I should see empty result
diff --git a/features/project/star.feature b/features/project/star.feature
deleted file mode 100644
index 618f44fe6dc..00000000000
--- a/features/project/star.feature
+++ /dev/null
@@ -1,39 +0,0 @@
-@project-stars
-Feature: Project Star
- Scenario: New projects have 0 stars
- Given public project "Community"
- When I visit project "Community" page
- Then The project has no stars
-
- Scenario: Empty projects show star count
- Given public empty project "Empty Public Project"
- When I visit empty project page
- Then The project has no stars
-
- Scenario: Signed off users can't star projects
- Given public project "Community"
- And I visit project "Community" page
- When I click on the star toggle button
- Then I redirected to sign in page
-
- @javascript
- Scenario: Signed in users can toggle star
- Given I sign in as "John Doe"
- And public project "Community"
- And I visit project "Community" page
- When I click on the star toggle button
- Then The project has 1 star
- When I click on the star toggle button
- Then The project has 0 stars
-
- @javascript
- Scenario: Star count sums stars
- Given I sign in as "John Doe"
- And public project "Community"
- And I visit project "Community" page
- And I click on the star toggle button
- And I logout
- And I sign in as "Mary Jane"
- And I visit project "Community" page
- When I click on the star toggle button
- Then The project has 2 stars
diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb
index f1288c15084..962e39dde9a 100644
--- a/features/steps/explore/projects.rb
+++ b/features/steps/explore/projects.rb
@@ -36,13 +36,13 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
end
step 'I should see project "Community" home page' do
- page.within '.navbar-gitlab .title' do
+ page.within '.breadcrumbs .breadcrumb-item-text' do
expect(page).to have_content 'Community'
end
end
step 'I should see project "Internal" home page' do
- page.within '.navbar-gitlab .title' do
+ page.within '.breadcrumbs .breadcrumb-item-text' do
expect(page).to have_content 'Internal'
end
end
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index f6559b6be2f..818bbb50d0e 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -47,7 +47,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end
step 'I click new milestone button' do
- click_link "New milestone"
+ page.within('.nav-controls') do
+ click_link "New milestone"
+ end
end
step 'I press create mileston button' do
diff --git a/features/steps/profile/emails.rb b/features/steps/profile/emails.rb
index 10ebe705365..4f44f932a6d 100644
--- a/features/steps/profile/emails.rb
+++ b/features/steps/profile/emails.rb
@@ -28,7 +28,7 @@ class Spinach::Features::ProfileEmails < Spinach::FeatureSteps
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"
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index 5cd9bd38c9d..1a18f1d7065 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -22,25 +22,25 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
step 'I click the "Edit Project"' do
- page.within '.sub-nav' do
+ page.within '.nav-sidebar' do
click_link('Edit Project')
end
end
step 'I click the "Integrations" tab' do
- page.within '.sub-nav' do
+ page.within '.nav-sidebar' do
click_link('Integrations')
end
end
step 'I click the "Repository" tab' do
- page.within '.sub-nav' do
+ page.within '.sidebar-top-level-items > .active' do
click_link('Repository')
end
end
step 'I click the "Activity" tab' do
- page.within '.sub-nav' do
+ page.within '.sidebar-top-level-items > .active' do
click_link('Activity')
end
end
@@ -72,7 +72,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
step 'I click the "Branches" tab' do
- page.within '.sub-nav' do
+ page.within '.nav-sidebar' do
click_link('Branches')
end
end
@@ -82,7 +82,7 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
step 'I click the "Charts" tab' do
- page.within '.sub-nav' do
+ page.within('.sidebar-top-level-items > .active') do
click_link('Charts')
end
end
@@ -102,13 +102,13 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
# Sub Tabs: Issues
step 'I click the "Milestones" sub tab' do
- page.within('.sub-nav') do
+ page.within('.nav-sidebar') do
click_link('Milestones')
end
end
step 'I click the "Labels" sub tab' do
- page.within('.sub-nav') do
+ page.within('.nav-sidebar') do
click_link('Labels')
end
end
diff --git a/features/steps/project/commits/user_lookup.rb b/features/steps/project/commits/user_lookup.rb
deleted file mode 100644
index 4599e0d032a..00000000000
--- a/features/steps/project/commits/user_lookup.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-class Spinach::Features::ProjectCommitsUserLookup < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
-
- step 'I click on commit link' do
- visit project_commit_path(@project, sample_commit.id)
- end
-
- step 'I click on another commit link' do
- visit project_commit_path(@project, sample_commit.parent_id)
- end
-
- step 'I have user with primary email' do
- user_primary
- end
-
- step 'I have user with secondary email' do
- user_secondary
- end
-
- step 'I see author based on primary email' do
- check_author_link(sample_commit.author_email, user_primary)
- end
-
- step 'I see author based on secondary email' do
- check_author_link(sample_commit.author_email, user_secondary)
- end
-
- def check_author_link(email, user)
- author_link = find('.commit-author-link')
-
- expect(author_link['href']).to eq user_path(user)
- expect(author_link['title']).to eq email
- expect(find('.commit-author-name').text).to eq user.name
- end
-
- def user_primary
- @user_primary ||= create(:user, email: 'dmitriy.zaporozhets@gmail.com')
- end
-
- def user_secondary
- @user_secondary ||= begin
- user = create(:user, email: 'dzaporozhets@example.com')
- create(:email, { user: user, email: 'dmitriy.zaporozhets@gmail.com' })
- user
- end
- end
-end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index dd4dff7f7a9..f88738b4c61 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -36,8 +36,8 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I goto the Merge Requests page' do
- page.within '.layout-nav' do
- click_link "Merge Requests"
+ page.within '.nav-sidebar' do
+ first(:link, "Merge Requests").click
end
end
@@ -52,7 +52,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I visit the forks page of the "Shop" project' do
- @project = Project.where(name: 'Shop').last
+ @project = Project.where(name: 'Shop').first
visit project_forks_path(@project)
end
diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb
index e78e25318a6..b9cddf4041d 100644
--- a/features/steps/project/graph.rb
+++ b/features/steps/project/graph.rb
@@ -38,9 +38,9 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
step 'page should have CI graphs' do
expect(page).to have_content 'Overall'
- expect(page).to have_content 'Jobs for last week'
- expect(page).to have_content 'Jobs for last month'
- expect(page).to have_content 'Jobs for last year'
+ expect(page).to have_content 'Pipelines for last week'
+ expect(page).to have_content 'Pipelines for last month'
+ expect(page).to have_content 'Pipelines for last year'
expect(page).to have_content 'Commit duration in minutes for last 30 commits'
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 2deef9036d3..b9460f5b534 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -168,6 +168,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
author: project.users.first,
description: "# Description header"
)
+ wait_for_requests
end
step 'project "Shop" have "Tweet control" open issue' do
diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb
index fe94eb03acd..16a2d4a6f93 100644
--- a/features/steps/project/issues/milestones.rb
+++ b/features/steps/project/issues/milestones.rb
@@ -16,7 +16,9 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
end
step 'I click link "New Milestone"' do
- click_link "New milestone"
+ page.within('.nav-controls') do
+ click_link "New milestone"
+ end
end
step 'I submit new milestone "v2.3"' do
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 810cd75591b..0d49a4ab90d 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -14,7 +14,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I click link "New Merge Request"' do
- page.within '#content-body' do
+ page.within '.nav-controls' do
page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
end
end
@@ -299,9 +299,6 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
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('.more-actions').click
- find('.more-actions .dropdown-menu li', match: :first)
-
find('.js-note-edit').click
page.within('.current-note-edit-form', visible: true) do
diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb
index 275fb4fc010..124a132d688 100644
--- a/features/steps/project/pages.rb
+++ b/features/steps/project/pages.rb
@@ -23,19 +23,23 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps
end
step 'I should see the "Pages" tab' do
- page.within '.sub-nav' do
+ page.within '.nav-sidebar' do
expect(page).to have_link('Pages')
end
end
step 'I should not see the "Pages" tab' do
- page.within '.sub-nav' do
+ page.within '.nav-sidebar' do
expect(page).not_to have_link('Pages')
end
end
step 'pages are deployed' do
- pipeline = @project.pipelines.create(ref: 'HEAD', sha: @project.commit('HEAD').sha)
+ pipeline = @project.pipelines.create(ref: 'HEAD',
+ sha: @project.commit('HEAD').sha,
+ source: :push,
+ protected: false)
+
build = build(:ci_build,
project: @project,
pipeline: pipeline,
@@ -43,6 +47,7 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps
artifacts_file: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip'),
artifacts_metadata: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta')
)
+
result = ::Projects::UpdatePagesService.new(@project, build).execute
expect(result[:status]).to eq(:success)
end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 0a89c1baf20..3a762be8f1f 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -6,7 +6,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps
step 'change project settings' do
fill_in 'project_name_edit', with: 'NewName'
- select 'Disabled', from: 'project_project_feature_attributes_issues_access_level'
end
step 'I save project' do
diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb
index a7d3352b8c4..b2d08515e77 100644
--- a/features/steps/project/project_milestone.rb
+++ b/features/steps/project/project_milestone.rb
@@ -55,7 +55,7 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps
end
step 'I click link "Labels"' do
- page.within('.layout-nav .nav-links') do
+ page.within('.nav-sidebar') do
page.find(:xpath, "//a[@href='#tab-labels']").click
end
end
diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb
index 53a2463af53..9ce86ca45d0 100644
--- a/features/steps/project/redirects.rb
+++ b/features/steps/project/redirects.rb
@@ -18,7 +18,7 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps
step 'I should see project "Community" home page' do
Gitlab.config.gitlab.should_receive(:host).and_return("www.example.com")
- page.within '.navbar-gitlab .title' do
+ page.within '.breadcrumbs .breadcrumb-item-text' do
expect(page).to have_content 'Community'
end
end
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index b0407d3f07d..2a1e6b2bce8 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -23,7 +23,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
end
step 'I click link "New snippet"' do
- page.within '#content-body' do
+ page.within '.nav-controls' do
first(:link, "New snippet").click
end
end
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index 243a0f54f7f..f6445b57ec0 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -218,7 +218,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
# Wiki
step 'I go to wiki page' do
- click_link "Wiki"
+ first(:link, "Wiki").click
expect(current_path).to eq project_wiki_path(@project, "home")
end
diff --git a/features/steps/project/source/search_code.rb b/features/steps/project/source/search_code.rb
deleted file mode 100644
index feee756d7ec..00000000000
--- a/features/steps/project/source/search_code.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class Spinach::Features::ProjectSourceSearchCode < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
-
- step 'I search for term "coffee"' do
- fill_in "search", with: "coffee"
- click_button "Go"
- end
-
- step 'I should see files from repository containing "coffee"' do
- expect(page).to have_content 'coffee'
- expect(page).to have_content 'CONTRIBUTING.md'
- end
-
- step 'I should see empty result' do
- expect(page).to have_content "We couldn't find any"
- end
-end
diff --git a/features/steps/project/star.rb b/features/steps/project/star.rb
deleted file mode 100644
index 9f7c748a3b7..00000000000
--- a/features/steps/project/star.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-class Spinach::Features::ProjectStar < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include SharedUser
-
- step "The project has no stars" do
- expect(page).not_to have_content '.toggle-star'
- end
-
- step "The project has 0 stars" do
- has_n_stars(0)
- end
-
- step "The project has 1 star" do
- has_n_stars(1)
- end
-
- step "The project has 2 stars" do
- has_n_stars(2)
- end
-
- # Requires @javascript
- step "I click on the star toggle button" do
- find(".star-btn", visible: true).click
- end
-
- step 'I redirected to sign in page' do
- expect(current_path).to eq new_user_session_path
- end
-
- protected
-
- def has_n_stars(n)
- expect(page).to have_css(".star-count", text: n, visible: true)
- end
-end
diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
index af5db05e9e8..104d024fee2 100644
--- a/features/steps/shared/active_tab.rb
+++ b/features/steps/shared/active_tab.rb
@@ -7,11 +7,11 @@ module SharedActiveTab
end
def ensure_active_main_tab(content)
- expect(find('.layout-nav li.active')).to have_content(content)
+ expect(find('.sidebar-top-level-items > li.active')).to have_content(content)
end
def ensure_active_sub_tab(content)
- expect(find('.sub-nav li.active')).to have_content(content)
+ expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)')).to have_content(content)
end
def ensure_active_sub_nav(content)
@@ -19,11 +19,11 @@ module SharedActiveTab
end
step 'no other main tabs should be active' do
- expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1)
+ expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
end
step 'no other sub tabs should be active' do
- expect(page).to have_selector('.sub-nav li.active', count: 1)
+ expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1)
end
step 'no other sub navs should be active' do
diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb
index de119f2c6c0..03bc7e798e0 100644
--- a/features/steps/shared/group.rb
+++ b/features/steps/shared/group.rb
@@ -36,14 +36,12 @@ module SharedGroup
protected
def is_member_of(username, groupname, role)
- @project_count ||= 0
user = User.find_by(name: username) || create(:user, name: username)
group = Group.find_by(name: groupname) || create(:group, name: groupname)
group.add_user(user, role)
- project ||= create(:project, :repository, namespace: group, path: "project#{@project_count}")
+ project ||= create(:project, :repository, namespace: group)
create(:closed_issue_event, project: project)
project.team << [user, :master]
- @project_count += 1
end
def owned_group
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index 80187b83fee..0cd7b506a95 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -11,8 +11,8 @@ module SharedNote
note = find('.note')
note.hover
- note.find('.more-actions').click
- note.find('.more-actions .dropdown-menu li', match: :first)
+ find('.more-actions').click
+ find('.more-actions .dropdown-menu li', match: :first)
find(".js-note-delete").click
end
@@ -137,7 +137,7 @@ module SharedNote
step 'The comment with the header should not have an ID' do
page.within(".note-body > .note-text") do
- expect(page).to have_content("Comment with a header")
+ expect(page).to have_content("Comment with a header")
expect(page).not_to have_css("#comment-with-a-header")
end
end
@@ -147,21 +147,23 @@ module SharedNote
note = find('.note')
note.hover
- note.find('.more-actions').click
- note.find('.more-actions .dropdown-menu li', match: :first)
-
note.find('.js-note-edit').click
end
+ page.find('.current-note-edit-form textarea')
+
page.within(".current-note-edit-form") do
fill_in 'note[note]', with: '+1 Awesome!'
click_button 'Save comment'
end
+ wait_for_requests
end
step 'I should see +1 in the description' do
page.within(".note") do
expect(page).to have_content("+1 Awesome!")
end
+
+ wait_for_requests
end
end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index 830263fd038..be69a96c3ee 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -304,10 +304,6 @@ module SharedPaths
visit project_commits_path(@project, 'stable', { limit: 5 })
end
- step 'I visit project source page' do
- visit project_tree_path(@project, root_ref)
- end
-
step 'I visit blob file from repo' do
visit project_blob_path(@project, File.join(sample_commit.id, sample_blob.path))
end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 00f7cded2ae..96cc0745e97 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -71,28 +71,14 @@ module SharedProject
step 'project "Shop" has push event' do
@project = Project.find_by(name: "Shop")
+ @event = create(:push_event, project: @project, author: @user)
- data = {
- before: Gitlab::Git::BLANK_SHA,
- after: "6d394385cf567f80a8fd85055db1ab4c5295806f",
- ref: "refs/heads/fix",
- user_id: @user.id,
- user_name: @user.name,
- repository: {
- name: @project.name,
- url: "localhost/rubinius",
- description: "",
- homepage: "localhost/rubinius",
- private: true
- }
- }
-
- @event = Event.create(
- project: @project,
- action: Event::PUSHED,
- data: data,
- author_id: @user.id
- )
+ create(:push_event_payload,
+ event: @event,
+ action: :created,
+ commit_to: '6d394385cf567f80a8fd85055db1ab4c5295806f',
+ ref: 'fix',
+ commit_count: 1)
end
step 'I should see project "Shop" activity feed' do
@@ -103,7 +89,7 @@ module SharedProject
step 'I should see project settings' do
expect(current_path).to eq edit_project_path(@project)
expect(page).to have_content("Project name")
- expect(page).to have_content("Sharing and permissions")
+ expect(page).to have_content("Permissions")
end
def current_project
diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb
index 901f7f76ee9..5a516ee33bc 100644
--- a/features/steps/shared/project_tab.rb
+++ b/features/steps/shared/project_tab.rb
@@ -5,7 +5,7 @@ module SharedProjectTab
include SharedActiveTab
step 'the active main tab should be Project' do
- ensure_active_main_tab('Project')
+ ensure_active_main_tab('Overview')
end
step 'the active main tab should be Repository' do
@@ -53,7 +53,7 @@ module SharedProjectTab
end
step 'the active sub tab should be Home' do
- ensure_active_sub_tab('Home')
+ ensure_active_sub_tab('Details')
end
step 'the active sub tab should be Activity' do
diff --git a/features/steps/user.rb b/features/steps/user.rb
index 59385a6ab59..321c1e942d5 100644
--- a/features/steps/user.rb
+++ b/features/steps/user.rb
@@ -17,14 +17,9 @@ class Spinach::Features::User < Spinach::FeatureSteps
Issues::CreateService.new(project, user, issue_params).execute
# Push code contribution
- push_params = {
- project: project,
- action: Event::PUSHED,
- author_id: user.id,
- data: { commit_count: 3 }
- }
-
- Event.create(push_params)
+ event = create(:push_event, project: project, author: user)
+
+ create(:push_event_payload, event: event, commit_count: 3)
end
step 'I should see contributed projects' do
@@ -38,6 +33,6 @@ class Spinach::Features::User < Spinach::FeatureSteps
end
def contributed_project
- @contributed_project ||= create(:project, :public)
+ @contributed_project ||= create(:project, :public, :empty_repo)
end
end
diff --git a/features/support/gitaly.rb b/features/support/gitaly.rb
new file mode 100644
index 00000000000..3cd5f4ce497
--- /dev/null
+++ b/features/support/gitaly.rb
@@ -0,0 +1,3 @@
+Spinach.hooks.before_scenario do
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+end
diff --git a/lib/after_commit_queue.rb b/lib/after_commit_queue.rb
new file mode 100644
index 00000000000..4750a2c373a
--- /dev/null
+++ b/lib/after_commit_queue.rb
@@ -0,0 +1,30 @@
+module AfterCommitQueue
+ extend ActiveSupport::Concern
+
+ included do
+ after_commit :_run_after_commit_queue
+ after_rollback :_clear_after_commit_queue
+ end
+
+ def run_after_commit(method = nil, &block)
+ _after_commit_queue << proc { self.send(method) } if method # rubocop:disable GitlabSecurity/PublicSend
+ _after_commit_queue << block if block
+ true
+ end
+
+ protected
+
+ def _run_after_commit_queue
+ while action = _after_commit_queue.pop
+ self.instance_eval(&action)
+ end
+ end
+
+ def _after_commit_queue
+ @after_commit_queue ||= []
+ end
+
+ def _clear_after_commit_queue
+ _after_commit_queue.clear
+ end
+end
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index cdacf9839e5..374b611f55e 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
+ resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc "Gets a list of access requests for a #{source_type}." do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::AccessRequester
@@ -67,10 +67,12 @@ module API
end
delete ":id/access_requests/:user_id" do
source = find_source(source_type, params[:id])
+ member = source.requesters.find_by!(user_id: params[:user_id])
- status 204
- ::Members::DestroyService.new(source, current_user, params)
- .execute(:requesters)
+ destroy_conditionally!(member) do
+ ::Members::DestroyService.new(source, current_user, params)
+ .execute(:requesters)
+ end
end
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 94df543853b..ee4e1688e12 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -2,6 +2,17 @@ module API
class API < Grape::API
include APIGuard
+ LOG_FILENAME = Rails.root.join("log", "api_json.log")
+
+ use GrapeLogging::Middleware::RequestLogger,
+ logger: Logger.new(LOG_FILENAME),
+ formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
+ include: [
+ GrapeLogging::Loggers::Response.new,
+ GrapeLogging::Loggers::FilterParameters.new,
+ GrapeLogging::Loggers::ClientEnv.new
+ ]
+
allow_access_with_scope :api
prefix :api
@@ -108,6 +119,7 @@ module API
mount ::API::Internal
mount ::API::Issues
mount ::API::Jobs
+ mount ::API::JobArtifacts
mount ::API::Keys
mount ::API::Labels
mount ::API::Lint
@@ -143,6 +155,7 @@ module API
mount ::API::Variables
mount ::API::GroupVariables
mount ::API::Version
+ mount ::API::Wikis
route :any, '*path' do
error!('404 Not Found', 404)
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 0d2d71e336a..c4c0fdda665 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -122,7 +122,7 @@ module API
error_classes = [MissingTokenError, TokenNotFoundError,
ExpiredError, RevokedError, InsufficientScopeError]
- base.send :rescue_from, *error_classes, oauth2_bearer_token_error_handler
+ base.__send__(:rescue_from, *error_classes, oauth2_bearer_token_error_handler) # rubocop:disable GitlabSecurity/PublicSend
end
def oauth2_bearer_token_error_handler
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 5a028fc9d0b..c3d93996816 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -12,7 +12,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
AWARDABLES.each do |awardable_params|
awardable_string = awardable_params[:type].pluralize
awardable_id_string = "#{awardable_params[:type]}_#{awardable_params[:find_by]}"
@@ -88,8 +88,7 @@ module API
unauthorized! unless award.user == current_user || current_user.admin?
- status 204
- award.destroy
+ destroy_conditionally!(award)
end
end
end
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index 5a2d7a681e3..366b0dc9a6f 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get all project boards' do
detail 'This feature was introduced in 8.13'
success Entities::Board
@@ -122,13 +122,13 @@ module API
end
delete "/lists/:list_id" do
authorize!(:admin_list, user_project)
-
list = board_lists.find(params[:list_id])
- service = ::Boards::Lists::DestroyService.new(user_project, current_user)
-
- unless service.execute(list)
- render_api_error!({ error: 'List could not be deleted!' }, 400)
+ destroy_conditionally!(list) do |list|
+ service = ::Boards::Lists::DestroyService.new(user_project, current_user)
+ unless service.execute(list)
+ render_api_error!({ error: 'List could not be deleted!' }, 400)
+ end
end
end
end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index d3dbf941298..642c1140fcc 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -24,17 +24,22 @@ module API
present paginate(branches), with: Entities::RepoBranch, project: user_project
end
- desc 'Get a single branch' do
- success Entities::RepoBranch
- end
- params do
- requires :branch, type: String, desc: 'The name of the branch'
- end
- get ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
- branch = user_project.repository.find_branch(params[:branch])
- not_found!("Branch") unless branch
+ resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
+ desc 'Get a single branch' do
+ success Entities::RepoBranch
+ end
+ params do
+ requires :branch, type: String, desc: 'The name of the branch'
+ end
+ head do
+ user_project.repository.branch_exists?(params[:branch]) ? status(204) : status(404)
+ end
+ get do
+ 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::RepoBranch, project: user_project
+ end
end
# Note: This API will be deprecated in favor of the protected branches API.
@@ -125,11 +130,18 @@ module API
delete ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
authorize_push_project
- result = DeleteBranchService.new(user_project, current_user)
- .execute(params[:branch])
+ branch = user_project.repository.find_branch(params[:branch])
+ not_found!('Branch') unless branch
+
+ commit = user_project.repository.commit(branch.dereferenced_target)
+
+ destroy_conditionally!(commit, last_updated: commit.authored_date) do
+ result = DeleteBranchService.new(user_project, current_user)
+ .execute(params[:branch])
- if result[:status] != :success
- render_api_error!(result[:message], result[:return_code])
+ if result[:status] != :success
+ render_api_error!(result[:message], result[:return_code])
+ end
end
end
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
index 9980aec4752..d7138b2f2fe 100644
--- a/lib/api/broadcast_messages.rb
+++ b/lib/api/broadcast_messages.rb
@@ -20,7 +20,7 @@ module API
use :pagination
end
get do
- messages = BroadcastMessage.all
+ messages = BroadcastMessage.all.order_id_desc
present paginate(messages), with: Entities::BroadcastMessage
end
@@ -91,8 +91,7 @@ module API
delete ':id' do
message = find_message
- status 204
- message.destroy
+ destroy_conditionally!(message)
end
end
end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 485b680cd5f..829eef18795 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -5,7 +5,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
include PaginationParams
before { authenticate! }
@@ -74,7 +74,8 @@ module API
source: :external,
sha: commit.sha,
ref: ref,
- user: current_user)
+ user: current_user,
+ protected: @project.protected_for?(ref))
end
status = GenericCommitStatus.running_or_pending.find_or_initialize_by(
@@ -82,7 +83,8 @@ module API
pipeline: pipeline,
name: name,
ref: ref,
- user: current_user
+ user: current_user,
+ protected: @project.protected_for?(ref)
)
optional_attributes =
@@ -101,7 +103,7 @@ module API
when 'success'
status.success!
when 'failed'
- status.drop!
+ status.drop!(:api_failure)
when 'canceled'
status.cancel!
else
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index ea78737288a..4b8d248f5f7 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -104,7 +104,7 @@ module API
not_found! 'Commit' unless commit
- commit.raw_diffs.to_a
+ present commit.raw_diffs.to_a, with: Entities::RepoDiff
end
desc "Get a commit's comments" do
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 42e7c1486b0..281269b1190 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -17,7 +17,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of the project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
before { authorize_admin_project }
desc "Get a specific project's deploy keys" do
@@ -125,8 +125,7 @@ module API
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
not_found!('Deploy Key') unless key
- status 204
- key.destroy
+ destroy_conditionally!(key)
end
end
end
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index 46b936897f6..1efee9a1324 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -8,7 +8,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get all deployments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Deployment
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 6ba4005dd0b..52c49e5caa9 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1,11 +1,21 @@
module API
module Entities
+ class WikiPageBasic < Grape::Entity
+ expose :format
+ expose :slug
+ expose :title
+ end
+
+ class WikiPage < WikiPageBasic
+ expose :content
+ end
+
class UserSafe < Grape::Entity
- expose :name, :username
+ expose :id, :name, :username
end
class UserBasic < UserSafe
- expose :id, :state
+ expose :state
expose :avatar_url do |user, options|
user.avatar_url(only_path: false)
end
@@ -35,7 +45,7 @@ module API
expose :confirmed_at
expose :last_activity_on
expose :email
- expose :color_scheme_id, :projects_limit, :current_sign_in_at
+ expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity
expose :can_create_group?, as: :can_create_group
expose :can_create_project?, as: :can_create_project
@@ -83,7 +93,7 @@ module API
expose :created_at, :last_activity_at
end
- class Project < BasicProjectDetails
+ class Project < BasicProjectDetails
include ::API::Helpers::RelatedResourcesHelpers
expose :_links do
@@ -119,6 +129,7 @@ module API
expose :archived?, as: :archived
expose :visibility
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
+ expose :resolve_outdated_diff_discussions
expose :container_registry_enabled
# Expose old field names with the new permissions methods to keep API compatible
@@ -132,7 +143,7 @@ module API
expose :lfs_enabled?, as: :lfs_enabled
expose :creator_id
expose :namespace, using: 'API::Entities::Namespace'
- expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
+ 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|
@@ -290,10 +301,11 @@ module API
end
class RepoDiff < Grape::Entity
- expose :old_path, :new_path, :a_mode, :b_mode, :diff
+ expose :old_path, :new_path, :a_mode, :b_mode
expose :new_file?, as: :new_file
expose :renamed_file?, as: :renamed_file
expose :deleted_file?, as: :deleted_file
+ expose :json_safe_diff, as: :diff
end
class ProtectedRefAccess < Grape::Entity
@@ -320,7 +332,10 @@ module API
end
class IssueBasic < ProjectEntity
- expose :label_names, as: :labels
+ expose :labels do |issue, options|
+ # Avoids an N+1 query since labels are preloaded
+ issue.labels.map(&:title).sort
+ end
expose :milestone, using: Entities::Milestone
expose :assignees, :author, using: Entities::UserBasic
@@ -329,13 +344,32 @@ module API
end
expose :user_notes_count
- expose :upvotes, :downvotes
+ expose :upvotes do |issue, options|
+ if options[:issuable_metadata]
+ # Avoids an N+1 query when metadata is included
+ options[:issuable_metadata][issue.id].upvotes
+ else
+ issue.upvotes
+ end
+ end
+ expose :downvotes do |issue, options|
+ if options[:issuable_metadata]
+ # Avoids an N+1 query when metadata is included
+ options[:issuable_metadata][issue.id].downvotes
+ else
+ issue.downvotes
+ end
+ end
expose :due_date
expose :confidential
expose :web_url do |issue, options|
Gitlab::UrlBuilder.build(issue)
end
+
+ expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |issue|
+ issue
+ end
end
class Issue < IssueBasic
@@ -365,10 +399,22 @@ module API
end
class IssuableTimeStats < Grape::Entity
+ format_with(:time_tracking_formatter) do |time_spent|
+ Gitlab::TimeTrackingFormatter.output(time_spent)
+ end
+
expose :time_estimate
expose :total_time_spent
expose :human_time_estimate
- expose :human_total_time_spent
+
+ with_options(format_with: :time_tracking_formatter) do
+ expose :total_time_spent, as: :human_total_time_spent
+ end
+
+ def total_time_spent
+ # Avoids an N+1 query since timelogs are preloaded
+ object.timelogs.map(&:time_spent).sum
+ end
end
class ExternalIssue < Grape::Entity
@@ -418,6 +464,10 @@ module API
expose :web_url do |merge_request, options|
Gitlab::UrlBuilder.build(merge_request)
end
+
+ expose :time_stats, using: 'API::Entities::IssuableTimeStats' do |merge_request|
+ merge_request
+ end
end
class MergeRequest < MergeRequestBasic
@@ -453,6 +503,10 @@ module API
expose :user, using: Entities::UserPublic
end
+ class GPGKey < Grape::Entity
+ expose :id, :key, :created_at
+ end
+
class Note < Grape::Entity
# Only Issue and MergeRequest have iid
NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze
@@ -497,14 +551,24 @@ module API
expose :author, using: Entities::UserBasic
end
+ class PushEventPayload < Grape::Entity
+ expose :commit_count, :action, :ref_type, :commit_from, :commit_to
+ expose :ref, :commit_title
+ end
+
class Event < Grape::Entity
- expose :title, :project_id, :action_name
+ expose :project_id, :action_name
expose :target_id, :target_iid, :target_type, :author_id
- expose :data, :target_title
+ expose :target_title
expose :created_at
expose :note, using: Entities::Note, if: ->(event, options) { event.note? }
expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author }
+ expose :push_event_payload,
+ as: :push_data,
+ using: PushEventPayload,
+ if: -> (event, _) { event.push? }
+
expose :author_username do |event, options|
event.author&.username
end
@@ -531,8 +595,9 @@ module API
target_url = "namespace_project_#{target_type}_url"
target_anchor = "note_#{todo.note_id}" if todo.note_id?
- Gitlab::Routing.url_helpers.public_send(target_url,
- todo.project.namespace, todo.project, todo.target, anchor: target_anchor)
+ Gitlab::Routing
+ .url_helpers
+ .public_send(target_url, todo.project.namespace, todo.project, todo.target, anchor: target_anchor) # rubocop:disable GitlabSecurity/PublicSend
end
expose :body
@@ -726,6 +791,7 @@ module API
expose :tag_list
expose :run_untagged
expose :locked
+ expose :access_level
expose :version, :revision, :platform, :architecture
expose :contacted_at
expose :token, if: lambda { |runner, options| options[:current_user].admin? || !runner.is_shared? }
@@ -769,7 +835,7 @@ module API
class Variable < Grape::Entity
expose :key, :value
- expose :protected?, as: :protected
+ expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) }
end
class Pipeline < PipelineBasic
@@ -790,6 +856,7 @@ module API
class PipelineScheduleDetails < PipelineSchedule
expose :last_pipeline, using: Entities::PipelineBasic
+ expose :variables, using: Entities::Variable
end
class EnvironmentBasic < Grape::Entity
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index c774a5c6685..5c63ec028d9 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get all environments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Environment
@@ -79,8 +79,7 @@ module API
environment = user_project.environments.find(params[:environment_id])
- status 204
- environment.destroy
+ destroy_conditionally!(environment)
end
desc 'Stops an existing environment' do
diff --git a/lib/api/events.rb b/lib/api/events.rb
index dabdf579119..b0713ff1d54 100644
--- a/lib/api/events.rb
+++ b/lib/api/events.rb
@@ -67,7 +67,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc "List a Project's visible events" do
success Entities::Event
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 450334fee84..1598d3c00b8 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -1,5 +1,10 @@
module API
class Files < Grape::API
+ FILE_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(file_path: API::NO_SLASH_URL_PART_REGEX)
+
+ # Prevents returning plain/text responses for files with .txt extension
+ after_validation { content_type "application/json" }
+
helpers do
def commit_params(attrs)
{
@@ -55,13 +60,13 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: FILE_ENDPOINT_REQUIREMENTS do
desc 'Get raw file contents from the repository'
params do
requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
requires :ref, type: String, desc: 'The name of branch, tag commit'
end
- get ":id/repository/files/:file_path/raw" do
+ get ":id/repository/files/:file_path/raw", requirements: FILE_ENDPOINT_REQUIREMENTS do
assign_file_vars!
send_git_blob @repo, @blob
@@ -72,7 +77,7 @@ module API
requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
requires :ref, type: String, desc: 'The name of branch, tag or commit'
end
- get ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
+ get ":id/repository/files/:file_path", requirements: FILE_ENDPOINT_REQUIREMENTS do
assign_file_vars!
{
@@ -92,7 +97,7 @@ module API
params do
use :extended_file_params
end
- post ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
+ post ":id/repository/files/:file_path", requirements: FILE_ENDPOINT_REQUIREMENTS do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
@@ -110,7 +115,7 @@ module API
params do
use :extended_file_params
end
- put ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
+ put ":id/repository/files/:file_path", requirements: FILE_ENDPOINT_REQUIREMENTS do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
@@ -134,7 +139,7 @@ module API
params do
use :simple_file_params
end
- delete ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
+ delete ":id/repository/files/:file_path", requirements: FILE_ENDPOINT_REQUIREMENTS do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb
index b85eb59dc0a..93fa0b95857 100644
--- a/lib/api/group_milestones.rb
+++ b/lib/api/group_milestones.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: { id: %r{[^/]+} } do
+ resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a list of group milestones' do
success Entities::Milestone
end
diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb
index f64da4ab77b..92800ce6450 100644
--- a/lib/api/group_variables.rb
+++ b/lib/api/group_variables.rb
@@ -9,7 +9,7 @@ module API
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: { id: %r{[^/]+} } do
+ resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get group-level variables' do
success Entities::Variable
end
@@ -88,8 +88,7 @@ module API
variable = user_group.variables.find_by(key: params[:key])
not_found!('GroupVariable') unless variable
- status 204
- variable.destroy
+ destroy_conditionally!(variable)
end
end
end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 49c3b2278c7..e817dcbbc4b 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -2,12 +2,16 @@ module API
class Groups < Grape::API
include PaginationParams
- before { authenticate! }
+ before { authenticate_non_get! }
helpers do
params :optional_params_ce do
optional :description, type: String, desc: 'The description of the group'
- optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the group'
+ optional :visibility, type: String,
+ values: Gitlab::VisibilityLevel.string_values,
+ default: Gitlab::VisibilityLevel.string_level(
+ Gitlab::CurrentSettings.current_application_settings.default_group_visibility),
+ desc: 'The visibility of the group'
optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group'
optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access'
optional :share_with_group_lock, type: Boolean, desc: 'Prevent sharing a project with another group within this group'
@@ -47,16 +51,8 @@ module API
use :pagination
end
get do
- groups = if params[:owned]
- current_user.owned_groups
- elsif current_user.admin
- Group.all
- elsif params[:all_available]
- GroupsFinder.new(current_user).execute
- else
- current_user.groups
- end
-
+ find_params = { all_available: params[:all_available], owned: params[:owned] }
+ groups = GroupsFinder.new(current_user, find_params).execute
groups = groups.search(params[:search]) if params[:search].present?
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort])
@@ -78,7 +74,12 @@ module API
use :optional_params
end
post do
- authorize! :create_group
+ parent_group = find_group!(params[:parent_id]) if params[:parent_id].present?
+ if parent_group
+ authorize! :create_subgroup, parent_group
+ else
+ authorize! :create_group
+ end
group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
@@ -93,7 +94,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: { id: %r{[^/]+} } do
+ resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Update a group. Available only for users who can administrate groups.' do
success Entities::Group
end
@@ -126,8 +127,9 @@ module API
group = find_group!(params[:id])
authorize! :admin_group, group
- status 204
- ::Groups::DestroyService.new(group, current_user).execute
+ destroy_conditionally!(group) do |group|
+ ::Groups::DestroyService.new(group, current_user).execute
+ end
end
desc 'Get a list of projects in this group.' do
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 4056d06bcc7..6a0be74623e 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -11,6 +11,27 @@ module API
declared(params, options).to_h.symbolize_keys
end
+ def check_unmodified_since!(last_modified)
+ if_unmodified_since = Time.parse(headers['If-Unmodified-Since']) rescue nil
+
+ if if_unmodified_since && last_modified && last_modified > if_unmodified_since
+ render_api_error!('412 Precondition Failed', 412)
+ end
+ end
+
+ def destroy_conditionally!(resource, last_updated: nil)
+ last_updated ||= resource.updated_at
+
+ check_unmodified_since!(last_updated)
+
+ status 204
+ if block_given?
+ yield resource
+ else
+ resource.destroy
+ end
+ end
+
def current_user
return @current_user if defined?(@current_user)
@@ -35,6 +56,12 @@ module API
@project ||= find_project!(params[:id])
end
+ def wiki_page
+ page = user_project.wiki.find_page(params[:slug])
+
+ page || not_found!('Wiki Page')
+ end
+
def available_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
end
@@ -66,7 +93,7 @@ module API
end
def find_group(id)
- if id =~ /^\d+$/
+ if id.to_s =~ /^\d+$/
Group.find_by(id: id)
else
Group.find_by_full_path(id)
@@ -107,6 +134,10 @@ module API
merge_request
end
+ def find_build!(id)
+ user_project.builds.find(id.to_i)
+ end
+
def authenticate!
unauthorized! unless current_user && can?(initial_current_user, :access_api)
end
@@ -139,6 +170,14 @@ module API
authorize! :admin_project, user_project
end
+ def authorize_read_builds!
+ authorize! :read_build, user_project
+ end
+
+ def authorize_update_builds!
+ authorize! :update_build, user_project
+ end
+
def require_gitlab_workhorse!
unless env['HTTP_GITLAB_WORKHORSE'].present?
forbidden!('Request should be executed via GitLab Workhorse')
@@ -189,7 +228,7 @@ module API
def bad_request!(attribute)
message = ["400 (Bad request)"]
- message << "\"" + attribute.to_s + "\" not given"
+ message << "\"" + attribute.to_s + "\" not given" if attribute
render_api_error!(message.join(' '), 400)
end
@@ -257,7 +296,15 @@ module API
message << " " << trace.join("\n ")
API.logger.add Logger::FATAL, message
- rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500)
+
+ response_message =
+ if Rails.env.test?
+ message
+ else
+ '500 Internal Server Error'
+ end
+
+ rack_response({ 'message' => response_message }.to_json, 500)
end
# project helpers
@@ -282,7 +329,7 @@ module API
def uploaded_file(field, uploads_path)
if params[field]
- bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename)
+ bad_request!("#{field} is not a file") unless params[field][:filename]
return params[field]
end
@@ -403,6 +450,10 @@ module API
header(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
end
+ def send_artifacts_entry(build, entry)
+ 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.
def define_params_for_grape_middleware
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index ecb79317093..4c0db4d42b1 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -42,6 +42,19 @@ module API
::Users::ActivityService.new(actor, 'Git SSH').execute if commands.include?(params[:action])
end
+ def merge_request_urls
+ ::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
+ end
+
+ def redis_ping
+ result = Gitlab::Redis::SharedState.with { |redis| redis.ping }
+
+ result == 'PONG'
+ rescue => e
+ Rails.logger.warn("GitLab: An unexpected error occurred in pinging to Redis: #{e}")
+ false
+ end
+
private
def set_project
diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb
index d9cae1501f8..a50ea0b52aa 100644
--- a/lib/api/helpers/members_helpers.rb
+++ b/lib/api/helpers/members_helpers.rb
@@ -1,8 +1,10 @@
+# rubocop:disable GitlabSecurity/PublicSend
+
module API
module Helpers
module MembersHelpers
def find_source(source_type, id)
- public_send("find_#{source_type}!", id)
+ public_send("find_#{source_type}!", id) # rubocop:disable GitlabSecurity/PublicSend
end
def authorize_admin_source!(source_type, source)
diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb
index 0764b58fb4c..95108292aac 100644
--- a/lib/api/helpers/pagination.rb
+++ b/lib/api/helpers/pagination.rb
@@ -11,7 +11,7 @@ module API
def add_pagination_headers(paginated_data)
header 'X-Total', paginated_data.total_count.to_s
- header 'X-Total-Pages', paginated_data.total_pages.to_s
+ header 'X-Total-Pages', total_pages(paginated_data).to_s
header 'X-Per-Page', paginated_data.limit_value.to_s
header 'X-Page', paginated_data.current_page.to_s
header 'X-Next-Page', paginated_data.next_page.to_s
@@ -26,20 +26,25 @@ module API
links = []
- request_params[:page] = paginated_data.current_page - 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") unless paginated_data.first_page?
+ request_params[:page] = paginated_data.prev_page
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="prev") if request_params[:page]
- request_params[:page] = paginated_data.current_page + 1
- links << %(<#{request_url}?#{request_params.to_query}>; rel="next") unless paginated_data.last_page?
+ request_params[:page] = paginated_data.next_page
+ links << %(<#{request_url}?#{request_params.to_query}>; rel="next") if request_params[:page]
request_params[:page] = 1
links << %(<#{request_url}?#{request_params.to_query}>; rel="first")
- request_params[:page] = paginated_data.total_pages
+ request_params[:page] = total_pages(paginated_data)
links << %(<#{request_url}?#{request_params.to_query}>; rel="last")
links.join(', ')
end
+
+ def total_pages(paginated_data)
+ # Ensure there is in total at least 1 page
+ [paginated_data.total_pages, 1].max
+ end
end
end
end
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index f8645e364ce..282af32ca94 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -1,6 +1,8 @@
module API
module Helpers
module Runner
+ include Gitlab::CurrentSettings
+
JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
JOB_TOKEN_PARAM = :token
UPDATE_RUNNER_EVERY = 10 * 60
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 8b007869dc3..c0fef56378f 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -68,7 +68,7 @@ module API
end
get "/merge_request_urls" do
- ::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
+ merge_request_urls
end
#
@@ -88,7 +88,8 @@ module API
{
api_version: API.version,
gitlab_version: Gitlab::VERSION,
- gitlab_rev: Gitlab::REVISION
+ gitlab_rev: Gitlab::REVISION,
+ redis: redis_ping
}
end
@@ -142,6 +143,14 @@ module API
{ success: true, recovery_codes: codes }
end
+ post '/pre_receive' do
+ status 200
+
+ reference_counter_increased = Gitlab::ReferenceCounter.new(params[:gl_repository]).increase
+
+ { reference_counter_increased: reference_counter_increased }
+ end
+
post "/notify_post_receive" do
status 200
@@ -155,6 +164,21 @@ module API
# render_api_error!(e, 500)
# end
end
+
+ post '/post_receive' do
+ status 200
+
+ PostReceive.perform_async(params[:gl_repository], params[:identifier],
+ params[:changes])
+ broadcast_message = BroadcastMessage.current&.last&.message
+ reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease
+
+ {
+ merge_request_urls: merge_request_urls,
+ broadcast_message: broadcast_message,
+ reference_counter_decreased: reference_counter_decreased
+ }
+ end
end
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 4cec1145f3a..1729df2aad0 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -4,6 +4,8 @@ module API
before { authenticate! }
+ helpers ::Gitlab::IssuableMetadata
+
helpers do
def find_issues(args = {})
args = params.merge(args)
@@ -13,6 +15,7 @@ module API
args[:label_name] = args.delete(:labels)
issues = IssuesFinder.new(current_user, args).execute
+ .preload(:assignees, :labels, :notes, :timelogs)
issues.reorder(args[:order_by] => args[:sort])
end
@@ -33,6 +36,7 @@ module API
optional :assignee_id, type: Integer, desc: 'Return issues which are assigned to the user with the given ID'
optional :scope, type: String, values: %w[created-by-me assigned-to-me all],
desc: 'Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`'
+ optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
use :pagination
end
@@ -65,14 +69,20 @@ module API
get do
issues = find_issues
- present paginate(issues), with: Entities::IssueBasic, current_user: current_user
+ options = {
+ with: Entities::IssueBasic,
+ current_user: current_user,
+ issuable_metadata: issuable_meta_data(issues, 'Issue')
+ }
+
+ present paginate(issues), options
end
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups, requirements: { id: %r{[^/]+} } do
+ resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a list of group issues' do
success Entities::IssueBasic
end
@@ -86,14 +96,20 @@ module API
issues = find_issues(group_id: group.id)
- present paginate(issues), with: Entities::IssueBasic, current_user: current_user
+ options = {
+ with: Entities::IssueBasic,
+ current_user: current_user,
+ issuable_metadata: issuable_meta_data(issues, 'Issue')
+ }
+
+ present paginate(issues), options
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
@@ -109,7 +125,14 @@ module API
issues = find_issues(project_id: project.id)
- present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project
+ options = {
+ with: Entities::IssueBasic,
+ current_user: current_user,
+ project: user_project,
+ issuable_metadata: issuable_meta_data(issues, 'Issue')
+ }
+
+ present paginate(issues), options
end
desc 'Get a single project issue' do
@@ -230,8 +253,8 @@ module API
not_found!('Issue') unless issue
authorize!(:destroy_issue, issue)
- status 204
- issue.destroy
+
+ destroy_conditionally!(issue)
end
desc 'List merge requests closing issue' do
diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb
new file mode 100644
index 00000000000..2a8fa7659bf
--- /dev/null
+++ b/lib/api/job_artifacts.rb
@@ -0,0 +1,80 @@
+module API
+ class JobArtifacts < Grape::API
+ before { authenticate_non_get! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ desc 'Download the artifacts file from a job' do
+ detail 'This feature was introduced in GitLab 8.10'
+ end
+ params do
+ requires :ref_name, type: String, desc: 'The ref from repository'
+ requires :job, type: String, desc: 'The name for the job'
+ end
+ get ':id/jobs/artifacts/:ref_name/download',
+ requirements: { ref_name: /.+/ } do
+ authorize_read_builds!
+
+ builds = user_project.latest_successful_builds_for(params[:ref_name])
+ latest_build = builds.find_by!(name: params[:job])
+
+ present_artifacts!(latest_build.artifacts_file)
+ end
+
+ desc 'Download the artifacts file from a job' do
+ detail 'This feature was introduced in GitLab 8.5'
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ get ':id/jobs/:job_id/artifacts' do
+ authorize_read_builds!
+
+ build = find_build!(params[:job_id])
+
+ present_artifacts!(build.artifacts_file)
+ end
+
+ desc 'Download a specific file from artifacts archive' do
+ detail 'This feature was introduced in GitLab 10.0'
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ requires :artifact_path, type: String, desc: 'Artifact path'
+ end
+ get ':id/jobs/:job_id/artifacts/*artifact_path', format: false do
+ authorize_read_builds!
+
+ build = find_build!(params[:job_id])
+ not_found! unless build.artifacts?
+
+ path = Gitlab::Ci::Build::Artifacts::Path
+ .new(params[:artifact_path])
+ bad_request! unless path.valid?
+
+ send_artifacts_entry(build, path)
+ end
+
+ desc 'Keep the artifacts to prevent them from being deleted' do
+ success Entities::Job
+ end
+ params do
+ requires :job_id, type: Integer, desc: 'The ID of a job'
+ end
+ post ':id/jobs/:job_id/artifacts/keep' do
+ authorize_update_builds!
+
+ build = find_build!(params[:job_id])
+ authorize!(:update_build, build)
+ return not_found!(build) unless build.artifacts?
+
+ build.keep_artifacts!
+
+ status 200
+ present build, with: Entities::Job
+ end
+ end
+ end
+end
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 8a67de10bca..3c1c412ba42 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
helpers do
params :optional_scope do
optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
@@ -16,9 +16,9 @@ module API
case scope
when String
[scope]
- when Hashie::Mash
+ when ::Hash
scope.values
- when Hashie::Array
+ when ::Array
scope
else
['unknown']
@@ -66,42 +66,11 @@ module API
get ':id/jobs/:job_id' do
authorize_read_builds!
- build = get_build!(params[:job_id])
+ build = find_build!(params[:job_id])
present build, with: Entities::Job
end
- desc 'Download the artifacts file from a job' do
- detail 'This feature was introduced in GitLab 8.5'
- end
- params do
- requires :job_id, type: Integer, desc: 'The ID of a job'
- end
- get ':id/jobs/:job_id/artifacts' do
- authorize_read_builds!
-
- build = get_build!(params[:job_id])
-
- present_artifacts!(build.artifacts_file)
- end
-
- desc 'Download the artifacts file from a job' do
- detail 'This feature was introduced in GitLab 8.10'
- end
- params do
- requires :ref_name, type: String, desc: 'The ref from repository'
- requires :job, type: String, desc: 'The name for the job'
- end
- get ':id/jobs/artifacts/:ref_name/download',
- requirements: { ref_name: /.+/ } do
- authorize_read_builds!
-
- builds = user_project.latest_successful_builds_for(params[:ref_name])
- latest_build = builds.find_by!(name: params[:job])
-
- present_artifacts!(latest_build.artifacts_file)
- end
-
# TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
# is saved in the DB instead of file). But before that, we need to consider how to replace the value of
# `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
@@ -112,7 +81,7 @@ module API
get ':id/jobs/:job_id/trace' do
authorize_read_builds!
- build = get_build!(params[:job_id])
+ build = find_build!(params[:job_id])
header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
content_type 'text/plain'
@@ -131,7 +100,7 @@ module API
post ':id/jobs/:job_id/cancel' do
authorize_update_builds!
- build = get_build!(params[:job_id])
+ build = find_build!(params[:job_id])
authorize!(:update_build, build)
build.cancel
@@ -148,7 +117,7 @@ module API
post ':id/jobs/:job_id/retry' do
authorize_update_builds!
- build = get_build!(params[:job_id])
+ build = find_build!(params[:job_id])
authorize!(:update_build, build)
return forbidden!('Job is not retryable') unless build.retryable?
@@ -166,7 +135,7 @@ module API
post ':id/jobs/:job_id/erase' do
authorize_update_builds!
- build = get_build!(params[:job_id])
+ build = find_build!(params[:job_id])
authorize!(:update_build, build)
return forbidden!('Job is not erasable!') unless build.erasable?
@@ -174,25 +143,6 @@ module API
present build, with: Entities::Job
end
- desc 'Keep the artifacts to prevent them from being deleted' do
- success Entities::Job
- end
- params do
- requires :job_id, type: Integer, desc: 'The ID of a job'
- end
- post ':id/jobs/:job_id/artifacts/keep' do
- authorize_update_builds!
-
- build = get_build!(params[:job_id])
- authorize!(:update_build, build)
- return not_found!(build) unless build.artifacts?
-
- build.keep_artifacts!
-
- status 200
- present build, with: Entities::Job
- end
-
desc 'Trigger a manual job' do
success Entities::Job
detail 'This feature was added in GitLab 8.11'
@@ -203,7 +153,7 @@ module API
post ":id/jobs/:job_id/play" do
authorize_read_builds!
- build = get_build!(params[:job_id])
+ build = find_build!(params[:job_id])
authorize!(:update_build, build)
bad_request!("Unplayable Job") unless build.playable?
@@ -216,14 +166,6 @@ module API
end
helpers do
- def find_build(id)
- user_project.builds.find_by(id: id.to_i)
- end
-
- def get_build!(id)
- find_build(id) || not_found!
- end
-
def filter_builds(builds, scope)
return builds if scope.nil? || scope.empty?
@@ -234,14 +176,6 @@ module API
builds.where(status: available_statuses && scope)
end
-
- def authorize_read_builds!
- authorize! :read_build, user_project
- end
-
- def authorize_update_builds!
- authorize! :update_build, user_project
- end
end
end
end
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index 4520c98d951..e41a1720ac1 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get all labels of the project' do
success Entities::Label
end
@@ -56,8 +56,7 @@ module API
label = user_project.labels.find_by(title: params[:name])
not_found!('Label') unless label
- status 204
- label.destroy
+ destroy_conditionally!(label)
end
desc 'Update an existing label. At least one optional parameter is required.' do
diff --git a/lib/api/members.rb b/lib/api/members.rb
index bb970b7cd54..22e4bdead41 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
+ resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Gets a list of group or project members viewable by the authenticated user.' do
success Entities::Member
end
@@ -93,11 +93,11 @@ module API
end
delete ":id/members/:user_id" do
source = find_source(source_type, params[:id])
- # Ensure that memeber exists
- source.members.find_by!(user_id: params[:user_id])
+ member = source.members.find_by!(user_id: params[:user_id])
- status 204
- ::Members::DestroyService.new(source, current_user, declared_params).execute
+ destroy_conditionally!(member) do
+ ::Members::DestroyService.new(source, current_user, declared_params).execute
+ end
end
end
end
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index 4b79eac2b8b..95ef8f42954 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -8,7 +8,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a list of merge request diff versions' do
detail 'This feature was introduced in GitLab 8.12.'
success Entities::MergeRequestDiff
@@ -21,7 +21,7 @@ module API
get ":id/merge_requests/:merge_request_iid/versions" do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
- present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff
+ present paginate(merge_request.merge_request_diffs.order_id_desc), with: Entities::MergeRequestDiff
end
desc 'Get a single merge request diff version' do
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 8810d4e441d..56d72d511da 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -21,7 +21,7 @@ module API
return merge_requests if args[:view] == 'simple'
merge_requests
- .preload(:notes, :author, :assignee, :milestone, :merge_request_diff, :labels)
+ .preload(:notes, :author, :assignee, :milestone, :merge_request_diff, :labels, :timelogs)
end
params :merge_requests_params do
@@ -40,6 +40,7 @@ module API
optional :assignee_id, type: Integer, desc: 'Return merge requests which are assigned to the user with the given ID'
optional :scope, type: String, values: %w[created-by-me assigned-to-me all],
desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`'
+ optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
use :pagination
end
end
@@ -72,7 +73,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
include TimeTrackingEndpoints
helpers do
@@ -164,8 +165,8 @@ module API
merge_request = find_project_merge_request(params[:merge_request_iid])
authorize!(:destroy_merge_request, merge_request)
- status 204
- merge_request.destroy
+
+ destroy_conditionally!(merge_request)
end
params do
diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb
index ef09d9505d2..c570eace862 100644
--- a/lib/api/milestone_responses.rb
+++ b/lib/api/milestone_responses.rb
@@ -28,7 +28,7 @@ module API
end
def list_milestones_for(parent)
- milestones = parent.milestones
+ milestones = parent.milestones.order_id_desc
milestones = Milestone.filter_by_state(milestones, params[:state])
milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present?
milestones = filter_by_search(milestones, params[:search]) if params[:search]
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 65ff89edf65..d6e7203adaf 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
NOTEABLE_TYPES.each do |noteable_type|
noteables_str = noteable_type.to_s.underscore.pluralize
@@ -129,17 +129,19 @@ module API
end
delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
note = user_project.notes.find(params[:note_id])
+
authorize! :admin_note, note
- status 204
- ::Notes::DestroyService.new(user_project, current_user).execute(note)
+ destroy_conditionally!(note) do |note|
+ ::Notes::DestroyService.new(user_project, current_user).execute(note)
+ end
end
end
end
helpers do
def find_project_noteable(noteables_str, noteable_id)
- public_send("find_project_#{noteables_str.singularize}", noteable_id)
+ public_send("find_project_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend
end
def noteable_read_ability_name(noteable)
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
index 5d113c94b22..bcc0833aa5c 100644
--- a/lib/api/notification_settings.rb
+++ b/lib/api/notification_settings.rb
@@ -54,7 +54,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
+ resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc "Get #{source_type} level notification level settings, defaults to Global" do
detail 'This feature was introduced in GitLab 8.12'
success Entities::NotificationSetting
diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb
index dbeaf9e17ef..37f32411296 100644
--- a/lib/api/pipeline_schedules.rb
+++ b/lib/api/pipeline_schedules.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get all pipeline schedules' do
success Entities::PipelineSchedule
end
@@ -31,10 +31,6 @@ module API
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
end
get ':id/pipeline_schedules/:pipeline_schedule_id' do
- authorize! :read_pipeline_schedule, user_project
-
- not_found!('PipelineSchedule') unless pipeline_schedule
-
present pipeline_schedule, with: Entities::PipelineScheduleDetails
end
@@ -74,9 +70,6 @@ module API
optional :active, type: Boolean, desc: 'The activation of pipeline schedule'
end
put ':id/pipeline_schedules/:pipeline_schedule_id' do
- authorize! :read_pipeline_schedule, user_project
-
- not_found!('PipelineSchedule') unless pipeline_schedule
authorize! :update_pipeline_schedule, pipeline_schedule
if pipeline_schedule.update(declared_params(include_missing: false))
@@ -93,9 +86,6 @@ module API
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
end
post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do
- authorize! :read_pipeline_schedule, user_project
-
- not_found!('PipelineSchedule') unless pipeline_schedule
authorize! :update_pipeline_schedule, pipeline_schedule
if pipeline_schedule.own!(current_user)
@@ -112,22 +102,84 @@ module API
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
end
delete ':id/pipeline_schedules/:pipeline_schedule_id' do
- authorize! :read_pipeline_schedule, user_project
+ authorize! :admin_pipeline_schedule, pipeline_schedule
+
+ destroy_conditionally!(pipeline_schedule)
+ end
+
+ desc 'Create a new pipeline schedule variable' do
+ success Entities::Variable
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ requires :key, type: String, desc: 'The key of the variable'
+ requires :value, type: String, desc: 'The value of the variable'
+ end
+ post ':id/pipeline_schedules/:pipeline_schedule_id/variables' do
+ authorize! :update_pipeline_schedule, pipeline_schedule
- not_found!('PipelineSchedule') unless pipeline_schedule
+ variable_params = declared_params(include_missing: false)
+ variable = pipeline_schedule.variables.create(variable_params)
+ if variable.persisted?
+ present variable, with: Entities::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ desc 'Edit a pipeline schedule variable' do
+ success Entities::Variable
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ requires :key, type: String, desc: 'The key of the variable'
+ optional :value, type: String, desc: 'The value of the variable'
+ end
+ put ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
+ authorize! :update_pipeline_schedule, pipeline_schedule
+
+ if pipeline_schedule_variable.update(declared_params(include_missing: false))
+ present pipeline_schedule_variable, with: Entities::Variable
+ else
+ render_validation_error!(pipeline_schedule_variable)
+ end
+ end
+
+ desc 'Delete a pipeline schedule variable' do
+ success Entities::Variable
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ requires :key, type: String, desc: 'The key of the variable'
+ end
+ delete ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
authorize! :admin_pipeline_schedule, pipeline_schedule
status :accepted
- present pipeline_schedule.destroy, with: Entities::PipelineScheduleDetails
+ present pipeline_schedule_variable.destroy, with: Entities::Variable
end
end
helpers do
def pipeline_schedule
@pipeline_schedule ||=
- user_project.pipeline_schedules
- .preload(:owner, :last_pipeline)
- .find_by(id: params.delete(:pipeline_schedule_id))
+ user_project
+ .pipeline_schedules
+ .preload(:owner, :last_pipeline)
+ .find_by(id: params.delete(:pipeline_schedule_id)).tap do |pipeline_schedule|
+ unless can?(current_user, :read_pipeline_schedule, pipeline_schedule)
+ not_found!('Pipeline Schedule')
+ end
+ end
+ end
+
+ def pipeline_schedule_variable
+ @pipeline_schedule_variable ||=
+ pipeline_schedule.variables.find_by(key: params[:key]).tap do |pipeline_schedule_variable|
+ unless pipeline_schedule_variable
+ not_found!('Pipeline Schedule Variable')
+ end
+ end
end
end
end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index e505cae3992..74b3376a1f3 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get all Pipelines of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::PipelineBasic
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 649dd891f56..86066e2b58f 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -24,7 +24,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get project hooks' do
success Entities::ProjectHook
end
@@ -96,8 +96,7 @@ module API
delete ":id/hooks/:hook_id" do
hook = user_project.hooks.find(params.delete(:hook_id))
- status 204
- hook.destroy
+ destroy_conditionally!(hook)
end
end
end
diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb
index 451998c726a..0cb209a02d0 100644
--- a/lib/api/project_milestones.rb
+++ b/lib/api/project_milestones.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a list of project milestones' do
success Entities::Milestone
end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index f3d905b0068..2ccda1c1aa1 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
@@ -116,8 +116,8 @@ module API
not_found!('Snippet') unless snippet
authorize! :admin_project_snippet, snippet
- status 204
- snippet.destroy
+
+ destroy_conditionally!(snippet)
end
desc 'Get a raw project snippet'
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 89dda88d3f5..7dc19788462 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -1,7 +1,6 @@
require_dependency 'declarative_policy'
module API
- # Projects API
class Projects < Grape::API
include PaginationParams
@@ -17,6 +16,7 @@ module API
optional :jobs_enabled, type: Boolean, desc: 'Flag indication if jobs are enabled'
optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
+ optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the project.'
@@ -96,7 +96,7 @@ module API
end
end
- resource :users, requirements: { user_id: %r{[^/]+} } do
+ resource :users, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a user projects' do
success Entities::BasicProjectDetails
end
@@ -184,7 +184,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a single project' do
success Entities::ProjectWithAccess
end
@@ -237,6 +237,7 @@ module API
at_least_one_of_ce =
[
:jobs_enabled,
+ :resolve_outdated_diff_discussions,
:container_registry_enabled,
:default_branch,
:description,
@@ -334,7 +335,10 @@ module API
desc 'Remove a project'
delete ":id" do
authorize! :remove_project, user_project
- ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
+
+ destroy_conditionally!(user_project) do
+ ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
+ end
accepted!
end
@@ -351,6 +355,8 @@ module API
if user_project.forked_from_project.nil?
user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
+
+ ::Projects::ForksCountService.new(forked_from_project).refresh_cache
else
render_api_error!("Project already forked", 409)
end
@@ -361,8 +367,7 @@ module API
authorize! :remove_fork_project, user_project
if user_project.forked?
- status 204
- user_project.forked_project_link.destroy
+ destroy_conditionally!(user_project.forked_project_link)
else
not_modified!
end
@@ -406,8 +411,7 @@ module API
link = user_project.project_group_links.find_by(group_id: params[:group_id])
not_found!('Group Link') unless link
- status 204
- link.destroy
+ destroy_conditionally!(link)
end
desc 'Upload a file'
diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb
index d742f2e18d0..15fcb9e8e27 100644
--- a/lib/api/protected_branches.rb
+++ b/lib/api/protected_branches.rb
@@ -61,7 +61,7 @@ module API
service_args = [user_project, current_user, protected_branch_params]
protected_branch = ::ProtectedBranches::CreateService.new(*service_args).execute
-
+
if protected_branch.persisted?
present protected_branch, with: Entities::ProtectedBranch, project: user_project
else
@@ -76,9 +76,7 @@ module API
delete ':id/protected_branches/:name', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
protected_branch = user_project.protected_branches.find_by!(name: params[:name])
- protected_branch.destroy
-
- status 204
+ destroy_conditionally!(protected_branch)
end
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 14d2bff9cb5..2255fb1b70d 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 88fc62d33df..a3987c560dd 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -45,8 +45,10 @@ module API
end
delete '/' do
authenticate_runner!
- status 204
- Ci::Runner.find_by_token(params[:token]).destroy
+
+ runner = Ci::Runner.find_by_token(params[:token])
+
+ destroy_conditionally!(runner)
end
desc 'Validates authentication credentials' do
@@ -78,7 +80,7 @@ module API
no_content! unless current_runner.active?
update_runner_info
- if current_runner.is_runner_queue_value_latest?(params[:last_update])
+ if current_runner.runner_queue_value_latest?(params[:last_update])
header 'X-GitLab-Last-Update', params[:last_update]
Gitlab::Metrics.add_event(:build_not_found_cached)
return no_content!
@@ -112,6 +114,8 @@ module API
requires :id, type: Integer, desc: %q(Job's ID)
optional :trace, type: String, desc: %q(Job's full trace)
optional :state, type: String, desc: %q(Job's status: success, failed)
+ optional :failure_reason, type: String, values: CommitStatus.failure_reasons.keys,
+ desc: %q(Job's failure_reason)
end
put '/:id' do
job = authenticate_job!
@@ -125,7 +129,7 @@ module API
when 'success'
job.success
when 'failed'
- job.drop
+ job.drop(params[:failure_reason] || :unknown_failure)
end
end
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 5bf5a18e42f..d3559ef71be 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -55,7 +55,9 @@ module API
optional :tag_list, type: Array[String], desc: 'The list of tags for a runner'
optional :run_untagged, type: Boolean, desc: 'Flag indicating the runner can execute untagged jobs'
optional :locked, type: Boolean, desc: 'Flag indicating the runner is locked'
- at_least_one_of :description, :active, :tag_list, :run_untagged, :locked
+ optional :access_level, type: String, values: Ci::Runner.access_levels.keys,
+ desc: 'The access_level of the runner'
+ at_least_one_of :description, :active, :tag_list, :run_untagged, :locked, :access_level
end
put ':id' do
runner = get_runner(params.delete(:id))
@@ -77,17 +79,17 @@ module API
end
delete ':id' do
runner = get_runner(params[:id])
+
authenticate_delete_runner!(runner)
- status 204
- runner.destroy!
+ destroy_conditionally!(runner)
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
before { authorize_admin_project }
desc 'Get runners available for project' do
@@ -135,8 +137,7 @@ module API
runner = runner_project.runner
forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
- status 204
- runner_project.destroy
+ destroy_conditionally!(runner_project)
end
end
@@ -153,7 +154,7 @@ module API
render_api_error!('Scope contains invalid value', 400)
end
- runners.send(scope)
+ runners.public_send(scope) # rubocop:disable GitlabSecurity/PublicSend
end
def get_runner(id)
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 843c05ae32e..2cbd0517dc3 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -601,7 +601,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
before { authenticate! }
before { authorize_admin_project }
@@ -656,12 +656,14 @@ module API
delete ":id/services/:service_slug" do
service = user_project.find_or_initialize_service(params[:service_slug].underscore)
- attrs = service_attributes(service).inject({}) do |hash, key|
- hash.merge!(key => nil)
- end
+ destroy_conditionally!(service) do
+ attrs = service_attributes(service).inject({}) do |hash, key|
+ hash.merge!(key => nil)
+ end
- unless service.update_attributes(attrs.merge(active: false))
- render_api_error!('400 Bad Request', 400)
+ unless service.update_attributes(attrs.merge(active: false))
+ render_api_error!('400 Bad Request', 400)
+ end
end
end
@@ -689,7 +691,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc "Trigger a slash command for #{service_slug}" do
detail 'Added in GitLab 8.13'
end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index d55a61fa638..851b226e9e5 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -29,6 +29,7 @@ module API
desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
optional :enabled_git_access_protocol, type: String, values: %w[ssh http nil], desc: 'Allow only the selected protocols to be used for Git access.'
+ optional :project_export_enabled, type: Boolean, desc: 'Enable project export'
optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled'
optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects'
optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB'
@@ -121,6 +122,13 @@ module API
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
+ ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
+ optional :"#{type}_key_restriction",
+ type: Integer,
+ values: KeyRestrictionValidator.supported_key_restrictions(type),
+ desc: "Restrictions on the complexity of uploaded #{type.upcase} keys. A value of #{ApplicationSetting::FORBIDDEN_KEY_VALUE} disables all #{type.upcase} keys."
+ end
+
optional(*::ApplicationSettingsHelper.visible_attributes)
at_least_one_of(*::ApplicationSettingsHelper.visible_attributes)
end
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index 35ece56c65c..00eb7c60f16 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -123,8 +123,7 @@ module API
authorize! :destroy_personal_snippet, snippet
- status 204
- snippet.destroy
+ destroy_conditionally!(snippet)
end
desc 'Get a raw snippet' do
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index 91567909998..b3e1e23031a 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -12,7 +12,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
requires :subscribable_id, type: String, desc: 'The ID of a resource'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
subscribable_types.each do |type, finder|
type_singularized = type.singularize
entity_class = Entities.const_get(type_singularized.camelcase)
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index c0179037440..6b6a03e3300 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -66,8 +66,7 @@ module API
hook = SystemHook.find_by(id: params[:id])
not_found!('System hook') unless hook
- status 204
- hook.destroy
+ destroy_conditionally!(hook)
end
end
end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 1333747cced..912415e3a7f 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -65,11 +65,18 @@ module API
delete ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS do
authorize_push_project
- result = ::Tags::DestroyService.new(user_project, current_user)
- .execute(params[:tag_name])
+ tag = user_project.repository.find_tag(params[:tag_name])
+ not_found!('Tag') unless tag
+
+ commit = user_project.repository.commit(tag.dereferenced_target)
+
+ destroy_conditionally!(commit, last_updated: commit.authored_date) do
+ result = ::Tags::DestroyService.new(user_project, current_user)
+ .execute(params[:tag_name])
- if result[:status] != :success
- render_api_error!(result[:message], result[:return_code])
+ if result[:status] != :success
+ render_api_error!(result[:message], result[:return_code])
+ end
end
end
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 0fc13b35d5b..f70bc0622b7 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -57,7 +57,7 @@ module API
end
get "templates/licenses" do
options = {
- featured: declared(params).popular.present? ? true : nil
+ featured: declared(params)[:popular].present? ? true : nil
}
licences = ::Kaminari.paginate_array(Licensee::License.all(options))
present paginate(licences), with: Entities::RepoLicense
@@ -71,7 +71,7 @@ module API
requires :name, type: String, desc: 'The name of the template'
end
get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do
- not_found!('License') unless Licensee::License.find(declared(params).name)
+ not_found!('License') unless Licensee::License.find(declared(params)[:name])
template = parsed_license_template
@@ -102,7 +102,7 @@ module API
requires :name, type: String, desc: 'The name of the template'
end
get "templates/#{template_type}/:name" do
- new_template = klass.find(declared(params).name)
+ new_template = klass.find(declared(params)[:name])
render_response(template_type, new_template)
end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 55191169dd4..ffccfebe752 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -12,7 +12,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
ISSUABLE_TYPES.each do |type, finder|
type_id_str = "#{type.singularize}_iid".to_sym
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index edfdb63d183..dd6801664b1 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -5,7 +5,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Trigger a GitLab project pipeline' do
success Entities::Pipeline
end
@@ -140,8 +140,7 @@ module API
trigger = user_project.triggers.find(params.delete(:trigger_id))
return not_found!('Trigger') unless trigger
- status 204
- trigger.destroy
+ destroy_conditionally!(trigger)
end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index a590f2692a2..1825c90a23b 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -79,22 +79,17 @@ module API
end
desc 'Get a single user' do
- success Entities::UserBasic
+ success Entities::User
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
get ":id" do
user = User.find_by(id: params[:id])
- not_found!('User') unless user
+ not_found!('User') unless user && can?(current_user, :read_user, user)
- if current_user && current_user.admin?
- present user, with: Entities::UserPublic
- elsif can?(current_user, :read_user, user)
- present user, with: Entities::User
- else
- render_api_error!("User not found.", 404)
- end
+ opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : {}
+ present user, opts
end
desc 'Create a user. Available only for admins.' do
@@ -235,10 +230,89 @@ module API
key = user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
+ destroy_conditionally!(key)
+ end
+
+ desc 'Add a GPG key to a specified user. Available only for admins.' do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key, type: String, desc: 'The new GPG key'
+ end
+ post ':id/gpg_keys' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params.delete(:id))
+ not_found!('User') unless user
+
+ key = user.gpg_keys.new(declared_params(include_missing: false))
+
+ if key.save
+ present key, with: Entities::GPGKey
+ else
+ render_validation_error!(key)
+ end
+ end
+
+ desc 'Get the GPG keys of a specified user. Available only for admins.' do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ use :pagination
+ end
+ get ':id/gpg_keys' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ present paginate(user.gpg_keys), with: Entities::GPGKey
+ end
+
+ desc 'Delete an existing GPG key from a specified user. Available only for admins.' do
+ detail 'This feature was added in GitLab 10.0'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key_id, type: Integer, desc: 'The ID of the GPG key'
+ end
+ delete ':id/gpg_keys/:key_id' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ key = user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
status 204
key.destroy
end
+ desc 'Revokes an existing GPG key from a specified user. Available only for admins.' do
+ detail 'This feature was added in GitLab 10.0'
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the user'
+ requires :key_id, type: Integer, desc: 'The ID of the GPG key'
+ end
+ post ':id/gpg_keys/:key_id/revoke' do
+ authenticated_as_admin!
+
+ user = User.find_by(id: params[:id])
+ not_found!('User') unless user
+
+ key = user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ key.revoke
+ status :accepted
+ end
+
desc 'Add an email address to a specified user. Available only for admins.' do
success Entities::Email
end
@@ -292,7 +366,11 @@ module API
email = user.emails.find_by(id: params[:email_id])
not_found!('Email') unless email
- Emails::DestroyService.new(user, email: email.email).execute
+ destroy_conditionally!(email) do |email|
+ Emails::DestroyService.new(current_user, email: email.email).execute
+ end
+
+ user.update_secondary_emails!
end
desc 'Delete a user. Available only for admins.' do
@@ -304,11 +382,13 @@ module API
end
delete ":id" do
authenticated_as_admin!
+
user = User.find_by(id: params[:id])
not_found!('User') unless user
- status 204
- user.delete_async(deleted_by: current_user, params: params)
+ destroy_conditionally!(user) do
+ user.delete_async(deleted_by: current_user, params: params)
+ end
end
desc 'Block a user. Available only for admins.'
@@ -408,8 +488,11 @@ module API
requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token'
end
delete ':impersonation_token_id' do
- status 204
- find_impersonation_token.revoke!
+ token = find_impersonation_token
+
+ destroy_conditionally!(token) do
+ token.revoke!
+ end
end
end
end
@@ -486,6 +569,75 @@ module API
key = current_user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
+ destroy_conditionally!(key)
+ end
+
+ desc "Get the currently authenticated user's GPG keys" do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ use :pagination
+ end
+ get 'gpg_keys' do
+ present paginate(current_user.gpg_keys), with: Entities::GPGKey
+ end
+
+ desc 'Get a single GPG key owned by currently authenticated user' do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the GPG key'
+ end
+ get 'gpg_keys/:key_id' do
+ key = current_user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ present key, with: Entities::GPGKey
+ end
+
+ desc 'Add a new GPG key to the currently authenticated user' do
+ detail 'This feature was added in GitLab 10.0'
+ success Entities::GPGKey
+ end
+ params do
+ requires :key, type: String, desc: 'The new GPG key'
+ end
+ post 'gpg_keys' do
+ key = current_user.gpg_keys.new(declared_params)
+
+ if key.save
+ present key, with: Entities::GPGKey
+ else
+ render_validation_error!(key)
+ end
+ end
+
+ desc 'Revoke a GPG key owned by currently authenticated user' do
+ detail 'This feature was added in GitLab 10.0'
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the GPG key'
+ end
+ post 'gpg_keys/:key_id/revoke' do
+ key = current_user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
+ key.revoke
+ status :accepted
+ end
+
+ desc 'Delete a GPG key from the currently authenticated user' do
+ detail 'This feature was added in GitLab 10.0'
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the SSH key'
+ end
+ delete 'gpg_keys/:key_id' do
+ key = current_user.gpg_keys.find_by(id: params[:key_id])
+ not_found!('GPG Key') unless key
+
status 204
key.destroy
end
@@ -538,8 +690,11 @@ module API
email = current_user.emails.find_by(id: params[:email_id])
not_found!('Email') unless email
- status 204
- Emails::DestroyService.new(current_user, email: email.email).execute
+ destroy_conditionally!(email) do |email|
+ Emails::DestroyService.new(current_user, email: email.email).execute
+ end
+
+ current_user.update_secondary_emails!
end
desc 'Get a list of user activities'
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
index 93ad9eb26b8..c189d486f50 100644
--- a/lib/api/v3/builds.rb
+++ b/lib/api/v3/builds.rb
@@ -16,7 +16,7 @@ module API
coerce_with: ->(scope) {
if scope.is_a?(String)
[scope]
- elsif scope.is_a?(Hashie::Mash)
+ elsif scope.is_a?(::Hash)
scope.values
else
['unknown']
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 773f667abe0..c928ce5265b 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -25,14 +25,24 @@ module API
expose(:downvote?) { |note| false }
end
+ class PushEventPayload < Grape::Entity
+ expose :commit_count, :action, :ref_type, :commit_from, :commit_to
+ expose :ref, :commit_title
+ end
+
class Event < Grape::Entity
- expose :title, :project_id, :action_name
+ expose :project_id, :action_name
expose :target_id, :target_type, :author_id
- expose :data, :target_title
+ expose :target_title
expose :created_at
expose :note, using: Entities::Note, if: ->(event, options) { event.note? }
expose :author, using: ::API::Entities::UserBasic, if: ->(event, options) { event.author }
+ expose :push_event_payload,
+ as: :push_data,
+ using: PushEventPayload,
+ if: -> (event, _) { event.push? }
+
expose :author_username do |event, options|
event.author&.username
end
@@ -54,6 +64,7 @@ module API
expose :owner, using: ::API::Entities::UserBasic, unless: ->(project, options) { project.group }
expose :name, :name_with_namespace
expose :path, :path_with_namespace
+ expose :resolve_outdated_diff_discussions
expose :container_registry_enabled
# Expose old field names with the new permissions methods to keep API compatible
@@ -68,7 +79,7 @@ module API
expose :lfs_enabled?, as: :lfs_enabled
expose :creator_id
expose :namespace, using: 'API::Entities::Namespace'
- expose :forked_from_project, using: ::API::Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
+ expose :forked_from_project, using: ::API::Entities::BasicProjectDetails, if: lambda { |project, options| project.forked? }
expose :avatar_url do |user, options|
user.avatar_url(only_path: false)
end
diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb
index 35f462e907b..22866fc2845 100644
--- a/lib/api/v3/merge_request_diffs.rb
+++ b/lib/api/v3/merge_request_diffs.rb
@@ -20,7 +20,7 @@ module API
get ":id/merge_requests/:merge_request_id/versions" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
- present merge_request.merge_request_diffs, with: ::API::Entities::MergeRequestDiff
+ present merge_request.merge_request_diffs.order_id_desc, with: ::API::Entities::MergeRequestDiff
end
desc 'Get a single merge request diff version' do
diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb
index 4c7061d4939..9be4cf9d22a 100644
--- a/lib/api/v3/milestones.rb
+++ b/lib/api/v3/milestones.rb
@@ -34,6 +34,7 @@ module API
milestones = user_project.milestones
milestones = filter_milestones_state(milestones, params[:state])
milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present?
+ milestones = milestones.order_id_desc
present paginate(milestones), with: ::API::Entities::Milestone
end
diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb
index 23fe95e42e4..d49772b92f2 100644
--- a/lib/api/v3/notes.rb
+++ b/lib/api/v3/notes.rb
@@ -22,7 +22,7 @@ module API
use :pagination
end
get ":id/#{noteables_str}/:noteable_id/notes" do
- noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+ noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend
if can?(current_user, noteable_read_ability_name(noteable), noteable)
# We exclude notes that are cross-references and that cannot be viewed
@@ -50,7 +50,7 @@ module API
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
end
get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
- noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+ noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend
note = noteable.notes.find(params[:note_id])
can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
@@ -76,7 +76,7 @@ module API
noteable_id: params[:noteable_id]
}
- noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+ noteable = user_project.public_send(noteables_str.to_sym).find(params[:noteable_id]) # rubocop:disable GitlabSecurity/PublicSend
if can?(current_user, noteable_read_ability_name(noteable), noteable)
if params[:created_at] && (current_user.admin? || user_project.owner == current_user)
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index eb090453b48..7c260b8d910 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -18,6 +18,7 @@ module API
optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled'
optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled'
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
+ optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project'
optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.'
@@ -119,7 +120,7 @@ module API
get do
authenticate!
- present_projects current_user.authorized_projects,
+ present_projects current_user.authorized_projects.order_id_desc,
with: ::API::V3::Entities::ProjectWithAccess
end
@@ -296,9 +297,9 @@ module API
use :optional_params
at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled,
:wiki_enabled, :builds_enabled, :snippets_enabled,
- :shared_runners_enabled, :container_registry_enabled,
- :lfs_enabled, :public, :visibility_level, :public_builds,
- :request_access_enabled, :only_allow_merge_if_build_succeeds,
+ :shared_runners_enabled, :resolve_outdated_diff_discussions,
+ :container_registry_enabled, :lfs_enabled, :public, :visibility_level,
+ :public_builds, :request_access_enabled, :only_allow_merge_if_build_succeeds,
:only_allow_merge_if_all_discussions_are_resolved, :path,
:default_branch
end
@@ -388,6 +389,8 @@ module API
if user_project.forked_from_project.nil?
user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
+
+ ::Projects::ForksCountService.new(forked_from_project).refresh_cache
else
render_api_error!("Project already forked", 409)
end
diff --git a/lib/api/v3/templates.rb b/lib/api/v3/templates.rb
index 4c577a8d2b7..2a2fb59045c 100644
--- a/lib/api/v3/templates.rb
+++ b/lib/api/v3/templates.rb
@@ -59,7 +59,7 @@ module API
end
get route do
options = {
- featured: declared(params).popular.present? ? true : nil
+ featured: declared(params)[:popular].present? ? true : nil
}
present Licensee::License.all(options), with: ::API::Entities::RepoLicense
end
@@ -76,7 +76,7 @@ module API
requires :name, type: String, desc: 'The name of the template'
end
get route, requirements: { name: /[\w\.-]+/ } do
- not_found!('License') unless Licensee::License.find(declared(params).name)
+ not_found!('License') unless Licensee::License.find(declared(params)[:name])
template = parsed_license_template
@@ -111,7 +111,7 @@ module API
requires :name, type: String, desc: 'The name of the template'
end
get route do
- new_template = klass.find(declared(params).name)
+ new_template = klass.find(declared(params)[:name])
render_response(template_type, new_template)
end
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
index e9d4c35307b..534911fde5c 100644
--- a/lib/api/v3/triggers.rb
+++ b/lib/api/v3/triggers.rb
@@ -16,25 +16,31 @@ module API
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do
- project = find_project(params[:id])
- trigger = Ci::Trigger.find_by_token(params[:token].to_s)
- not_found! unless project && trigger
- unauthorized! unless trigger.project == project
-
# validate variables
- variables = params[:variables].to_h
- unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ params[:variables] = params[:variables].to_h
+ unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
render_api_error!('variables needs to be a map of key-valued strings', 400)
end
- # create request and trigger builds
- result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref].to_s, variables)
- pipeline = result.pipeline
+ project = find_project(params[:id])
+ not_found! unless project
+
+ result = Ci::PipelineTriggerService.new(project, nil, params).execute
+ not_found! unless result
- if pipeline.persisted?
- present result.trigger_request, with: ::API::V3::Entities::TriggerRequest
+ if result[:http_status]
+ render_api_error!(result[:message], result[:http_status])
else
- render_validation_error!(pipeline)
+ pipeline = result[:pipeline]
+
+ # We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables.
+ # Ci::TriggerRequest doesn't save variables anymore.
+ # Here is copying Ci::PipelineVariable to Ci::TriggerRequest.variables for presenting the variables.
+ # The same endpoint in v4 API pressents Pipeline instead of TriggerRequest, so it doesn't need such a process.
+ trigger_request = pipeline.trigger_requests.last
+ trigger_request.variables = params[:variables]
+
+ present trigger_request, with: ::API::V3::Entities::TriggerRequest
end
end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index 7c0fdd3d1be..d08876ae1b9 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -9,7 +9,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get project variables' do
success Entities::Variable
end
@@ -88,6 +88,7 @@ module API
variable = user_project.variables.find_by(key: params[:key])
not_found!('Variable') unless variable
+ # Variables don't have any timestamp. Therfore, destroy unconditionally.
status 204
variable.destroy
end
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
new file mode 100644
index 00000000000..b3fc4e876ad
--- /dev/null
+++ b/lib/api/wikis.rb
@@ -0,0 +1,89 @@
+module API
+ class Wikis < Grape::API
+ helpers do
+ params :wiki_page_params do
+ requires :content, type: String, desc: 'Content of a wiki page'
+ requires :title, type: String, desc: 'Title of a wiki page'
+ optional :format,
+ type: String,
+ values: ProjectWiki::MARKUPS.values.map(&:to_s),
+ default: 'markdown',
+ desc: 'Format of a wiki page. Available formats are markdown, rdoc, and asciidoc'
+ end
+ end
+
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ desc 'Get a list of wiki pages' do
+ success Entities::WikiPageBasic
+ end
+ params do
+ optional :with_content, type: Boolean, default: false, desc: "Include pages' content"
+ end
+ get ':id/wikis' do
+ authorize! :read_wiki, user_project
+
+ entity = params[:with_content] ? Entities::WikiPage : Entities::WikiPageBasic
+ present user_project.wiki.pages, with: entity
+ end
+
+ desc 'Get a wiki page' do
+ success Entities::WikiPage
+ end
+ params do
+ requires :slug, type: String, desc: 'The slug of a wiki page'
+ end
+ get ':id/wikis/:slug' do
+ authorize! :read_wiki, user_project
+
+ present wiki_page, with: Entities::WikiPage
+ end
+
+ desc 'Create a wiki page' do
+ success Entities::WikiPage
+ end
+ params do
+ use :wiki_page_params
+ end
+ post ':id/wikis' do
+ authorize! :create_wiki, user_project
+
+ page = WikiPages::CreateService.new(user_project, current_user, params).execute
+
+ if page.valid?
+ present page, with: Entities::WikiPage
+ else
+ render_validation_error!(page)
+ end
+ end
+
+ desc 'Update a wiki page' do
+ success Entities::WikiPage
+ end
+ params do
+ use :wiki_page_params
+ end
+ put ':id/wikis/:slug' do
+ authorize! :create_wiki, user_project
+
+ page = WikiPages::UpdateService.new(user_project, current_user, params).execute(wiki_page)
+
+ if page.valid?
+ present page, with: Entities::WikiPage
+ else
+ render_validation_error!(page)
+ end
+ end
+
+ desc 'Delete a wiki page'
+ params do
+ requires :slug, type: String, desc: 'The slug of a wiki page'
+ end
+ delete ':id/wikis/:slug' do
+ authorize! :admin_wiki, user_project
+
+ status 204
+ WikiPages::DestroyService.new(user_project, current_user).execute(wiki_page)
+ end
+ end
+ end
+end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index ca6d6848d41..b9a573d3542 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -198,11 +198,11 @@ module Backup
end
def archives_to_backup
- ARCHIVES_TO_BACKUP.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact
+ ARCHIVES_TO_BACKUP.map { |name| (name + ".tar.gz") unless skipped?(name) }.compact
end
def folders_to_backup
- FOLDERS_TO_BACKUP.reject{ |name| skipped?(name) }
+ FOLDERS_TO_BACKUP.reject { |name| skipped?(name) }
end
def disabled_features
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 88821ae56e0..4e92be85110 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -75,7 +75,7 @@ module Backup
path_to_project_repo = path_to_repo(project)
path_to_project_bundle = path_to_bundle(project)
- project.ensure_storage_path_exist
+ 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})
diff --git a/lib/banzai/commit_renderer.rb b/lib/banzai/commit_renderer.rb
new file mode 100644
index 00000000000..f5ff95e3eb3
--- /dev/null
+++ b/lib/banzai/commit_renderer.rb
@@ -0,0 +1,11 @@
+module Banzai
+ module CommitRenderer
+ ATTRIBUTES = [:description, :title].freeze
+
+ def self.render(commits, project, user = nil)
+ obj_renderer = ObjectRenderer.new(project, user)
+
+ ATTRIBUTES.each { |attr| obj_renderer.render(commits, attr) }
+ end
+ end
+end
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index 53a229256a5..ed01a72ff9f 100644
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -95,10 +95,10 @@ module Banzai
private
def external_issues_cached(attribute)
- return project.public_send(attribute) unless RequestStore.active?
+ return project.public_send(attribute) unless RequestStore.active? # rubocop:disable GitlabSecurity/PublicSend
cached_attributes = RequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} }
- cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil?
+ cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil? # rubocop:disable GitlabSecurity/PublicSend
cached_attributes[project.id][attribute]
end
end
diff --git a/lib/banzai/filter/image_lazy_load_filter.rb b/lib/banzai/filter/image_lazy_load_filter.rb
index 7a81d583b82..bcb4f332267 100644
--- a/lib/banzai/filter/image_lazy_load_filter.rb
+++ b/lib/banzai/filter/image_lazy_load_filter.rb
@@ -6,9 +6,9 @@ module Banzai
doc.xpath('descendant-or-self::img').each do |img|
img['class'] ||= '' << 'lazy'
img['data-src'] = img['src']
- img['src'] = LazyImageTagHelper.placeholder_image
+ img['src'] = LazyImageTagHelper.placeholder_image
end
-
+
doc
end
end
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index 8e7084f2543..47151626208 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -22,40 +22,94 @@ module Banzai
result[:toc] = ""
headers = Hash.new(0)
+ header_root = current_header = HeaderNode.new
doc.css('h1, h2, h3, h4, h5, h6').each do |node|
- text = node.text
+ if header_content = node.children.first
+ id = node
+ .text
+ .downcase
+ .gsub(PUNCTUATION_REGEXP, '') # remove punctuation
+ .tr(' ', '-') # replace spaces with dash
+ .squeeze('-') # replace multiple dashes with one
- id = text.downcase
- id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation
- id.tr!(' ', '-') # replace spaces with dash
- id.squeeze!('-') # replace multiple dashes with one
+ uniq = headers[id] > 0 ? "-#{headers[id]}" : ''
+ headers[id] += 1
+ href = "#{id}#{uniq}"
- uniq = (headers[id] > 0) ? "-#{headers[id]}" : ''
- headers[id] += 1
+ current_header = HeaderNode.new(node: node, href: href, previous_header: current_header)
- if header_content = node.children.first
- # namespace detection will be automatically handled via javascript (see issue #22781)
- namespace = "user-content-"
- href = "#{id}#{uniq}"
- push_toc(href, text)
- header_content.add_previous_sibling(anchor_tag("#{namespace}#{href}", href))
+ header_content.add_previous_sibling(anchor_tag(href))
end
end
- result[:toc] = %Q{<ul class="section-nav">\n#{result[:toc]}</ul>} unless result[:toc].empty?
+ push_toc(header_root.children, root: true)
doc
end
private
- def anchor_tag(id, href)
- %Q{<a id="#{id}" class="anchor" href="##{href}" aria-hidden="true"></a>}
+ def anchor_tag(href)
+ %Q{<a id="user-content-#{href}" class="anchor" href="##{href}" aria-hidden="true"></a>}
end
- def push_toc(href, text)
- result[:toc] << %Q{<li><a href="##{href}">#{text}</a></li>\n}
+ def push_toc(children, root: false)
+ return if children.empty?
+
+ klass = ' class="section-nav"' if root
+
+ result[:toc] << "<ul#{klass}>"
+ children.each { |child| push_anchor(child) }
+ result[:toc] << '</ul>'
+ end
+
+ def push_anchor(header_node)
+ result[:toc] << %Q{<li><a href="##{header_node.href}">#{header_node.text}</a>}
+ push_toc(header_node.children)
+ result[:toc] << '</li>'
+ end
+
+ class HeaderNode
+ attr_reader :node, :href, :parent, :children
+
+ def initialize(node: nil, href: nil, previous_header: nil)
+ @node = node
+ @href = href
+ @children = []
+
+ @parent = find_parent(previous_header)
+ @parent.children.push(self) if @parent
+ end
+
+ def level
+ return 0 unless node
+
+ @level ||= node.name[1].to_i
+ end
+
+ def text
+ return '' unless node
+
+ @text ||= node.text
+ end
+
+ private
+
+ def find_parent(previous_header)
+ return unless previous_header
+
+ if level == previous_header.level
+ parent = previous_header.parent
+ elsif level > previous_header.level
+ parent = previous_header
+ else
+ parent = previous_header
+ parent = parent.parent while parent.level >= level
+ end
+
+ parent
+ end
end
end
end
diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb
index 002a3341ccd..e40556e869c 100644
--- a/lib/banzai/object_renderer.rb
+++ b/lib/banzai/object_renderer.rb
@@ -37,8 +37,8 @@ module Banzai
objects.each_with_index do |object, index|
redacted_data = redacted[index]
- object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe)
- object.user_visible_reference_count = redacted_data[:visible_reference_count]
+ object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe) # rubocop:disable GitlabSecurity/PublicSend
+ object.user_visible_reference_count = redacted_data[:visible_reference_count] if object.respond_to?(:user_visible_reference_count)
end
end
diff --git a/lib/banzai/pipeline/base_pipeline.rb b/lib/banzai/pipeline/base_pipeline.rb
index 321fd5bbe14..3ae3bed570d 100644
--- a/lib/banzai/pipeline/base_pipeline.rb
+++ b/lib/banzai/pipeline/base_pipeline.rb
@@ -18,7 +18,7 @@ module Banzai
define_method(meth) do |text, context|
context = transform_context(context)
- html_pipeline.send(meth, text, context)
+ html_pipeline.__send__(meth, text, context) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index ad08c0905e2..ceca9296851 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -36,6 +36,10 @@ module Banzai
# The context to use is managed by the object and cannot be changed.
# Use #render, passing it the field text, if a custom rendering is needed.
def self.render_field(object, field)
+ unless object.respond_to?(:cached_markdown_fields)
+ 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.cached_html_for(field)
@@ -43,7 +47,7 @@ module Banzai
# Same as +render_field+, but without consulting or updating the cache field
def self.cacheless_render_field(object, field, options = {})
- text = object.__send__(field)
+ text = object.__send__(field) # rubocop:disable GitlabSecurity/PublicSend
context = object.banzai_render_context(field).merge(options)
cacheless_render(text, context)
@@ -156,7 +160,7 @@ module Banzai
# method.
def self.full_cache_multi_key(cache_key, pipeline_name)
return unless cache_key
- Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name))
+ 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
diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb
index 3a9379ff680..a78495dbf5e 100644
--- a/lib/bitbucket/collection.rb
+++ b/lib/bitbucket/collection.rb
@@ -13,7 +13,7 @@ module Bitbucket
def method_missing(method, *args)
return super unless self.respond_to?(method)
- self.send(method, *args) do |item|
+ self.__send__(method, *args) do |item| # rubocop:disable GitlabSecurity/PublicSend
block_given? ? yield(item) : item
end
end
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
index 55402101e43..b9e9f9f7f4a 100644
--- a/lib/ci/ansi2html.rb
+++ b/lib/ci/ansi2html.rb
@@ -208,7 +208,7 @@ module Ci
return unless command = stack.shift()
if self.respond_to?("on_#{command}", true)
- self.send("on_#{command}", stack)
+ self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend
end
evaluate_command_stack(stack)
@@ -254,7 +254,7 @@ module Ci
def state
state = STATE_PARAMS.inject({}) do |h, param|
- h[param] = send(param)
+ h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend
h
end
Base64.urlsafe_encode64(state.to_json)
@@ -266,7 +266,7 @@ module Ci
return if state[:offset].to_i > stream.size
STATE_PARAMS.each do |param|
- send("#{param}=".to_sym, state[param])
+ send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
deleted file mode 100644
index 24bb3649a76..00000000000
--- a/lib/ci/api/api.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-module Ci
- module API
- class API < Grape::API
- include ::API::APIGuard
- version 'v1', using: :path
-
- rescue_from ActiveRecord::RecordNotFound do
- rack_response({ 'message' => '404 Not found' }.to_json, 404)
- end
-
- # Retain 405 error rather than a 500 error for Grape 0.15.0+.
- # https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes
- rescue_from Grape::Exceptions::MethodNotAllowed do |e|
- error! e.message, e.status, e.headers
- end
-
- rescue_from Grape::Exceptions::Base do |e|
- error! e.message, e.status, e.headers
- end
-
- rescue_from :all do |exception|
- handle_api_exception(exception)
- end
-
- content_type :txt, 'text/plain'
- content_type :json, 'application/json'
- format :json
-
- helpers ::SentryHelper
- helpers ::Ci::API::Helpers
- helpers ::API::Helpers
- helpers Gitlab::CurrentSettings
-
- mount ::Ci::API::Builds
- mount ::Ci::API::Runners
- mount ::Ci::API::Triggers
- end
- end
-end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
deleted file mode 100644
index 812ba8ed603..00000000000
--- a/lib/ci/api/builds.rb
+++ /dev/null
@@ -1,219 +0,0 @@
-module Ci
- module API
- # Builds API
- class Builds < Grape::API
- resource :builds do
- # Runs oldest pending build by runner - Runners only
- #
- # Parameters:
- # token (required) - The uniq token of runner
- #
- # Example Request:
- # POST /builds/register
- post "register" do
- authenticate_runner!
- required_attributes! [:token]
- not_found! unless current_runner.active?
- update_runner_info
-
- if current_runner.is_runner_queue_value_latest?(params[:last_update])
- header 'X-GitLab-Last-Update', params[:last_update]
- Gitlab::Metrics.add_event(:build_not_found_cached)
- return build_not_found!
- end
-
- new_update = current_runner.ensure_runner_queue_value
-
- result = Ci::RegisterJobService.new(current_runner).execute
-
- if result.valid?
- if result.build
- Gitlab::Metrics.add_event(:build_found,
- project: result.build.project.full_path)
-
- present result.build, with: Entities::BuildDetails
- else
- Gitlab::Metrics.add_event(:build_not_found)
-
- header 'X-GitLab-Last-Update', new_update
-
- build_not_found!
- end
- else
- # We received build that is invalid due to concurrency conflict
- Gitlab::Metrics.add_event(:build_invalid)
- conflict!
- end
- end
-
- # Update an existing build - Runners only
- #
- # Parameters:
- # id (required) - The ID of a project
- # state (optional) - The state of a build
- # trace (optional) - The trace of a build
- # Example Request:
- # PUT /builds/:id
- put ":id" do
- authenticate_runner!
- build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id])
- validate_build!(build)
-
- update_runner_info
-
- build.trace.set(params[:trace]) if params[:trace]
-
- Gitlab::Metrics.add_event(:update_build,
- project: build.project.full_path)
-
- case params[:state].to_s
- when 'success'
- build.success
- when 'failed'
- build.drop
- end
- end
-
- # Send incremental log update - Runners only
- #
- # Parameters:
- # id (required) - The ID of a build
- # Body:
- # content of logs to append
- # Headers:
- # Content-Range (required) - range of content that was sent
- # BUILD-TOKEN (required) - The build authorization token
- # Example Request:
- # PATCH /builds/:id/trace.txt
- patch ":id/trace.txt" do
- build = authenticate_build!
-
- error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
- content_range = request.headers['Content-Range']
- content_range = content_range.split('-')
-
- stream_size = build.trace.append(request.body.read, content_range[0].to_i)
- if stream_size < 0
- return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{-stream_size}" })
- end
-
- status 202
- header 'Build-Status', build.status
- header 'Range', "0-#{stream_size}"
- end
-
- # Authorize artifacts uploading for build - Runners only
- #
- # Parameters:
- # id (required) - The ID of a build
- # token (required) - The build authorization token
- # filesize (optional) - the size of uploaded file
- # Example Request:
- # POST /builds/:id/artifacts/authorize
- post ":id/artifacts/authorize" do
- require_gitlab_workhorse!
- Gitlab::Workhorse.verify_api_request!(headers)
- not_allowed! unless Gitlab.config.artifacts.enabled
- build = authenticate_build!
- forbidden!('build is not running') unless build.running?
-
- if params[:filesize]
- file_size = params[:filesize].to_i
- file_to_large! unless file_size < max_artifacts_size
- end
-
- status 200
- content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
- Gitlab::Workhorse.artifact_upload_ok
- end
-
- # Upload artifacts to build - Runners only
- #
- # Parameters:
- # id (required) - The ID of a build
- # token (required) - The build authorization token
- # file (required) - Artifacts file
- # expire_in (optional) - Specify when artifacts should expire (ex. 7d)
- # Parameters (accelerated by GitLab Workhorse):
- # file.path - path to locally stored body (generated by Workhorse)
- # file.name - real filename as send in Content-Disposition
- # file.type - real content type as send in Content-Type
- # metadata.path - path to locally stored body (generated by Workhorse)
- # metadata.name - filename (generated by Workhorse)
- # Headers:
- # BUILD-TOKEN (required) - The build authorization token, the same as token
- # Body:
- # The file content
- #
- # Example Request:
- # POST /builds/:id/artifacts
- post ":id/artifacts" do
- require_gitlab_workhorse!
- not_allowed! unless Gitlab.config.artifacts.enabled
- build = authenticate_build!
- forbidden!('Build is not running!') unless build.running?
-
- artifacts_upload_path = ArtifactUploader.artifacts_upload_path
- artifacts = uploaded_file(:file, artifacts_upload_path)
- metadata = uploaded_file(:metadata, artifacts_upload_path)
-
- bad_request!('Missing artifacts file!') unless artifacts
- file_to_large! unless artifacts.size < max_artifacts_size
-
- build.artifacts_file = artifacts
- build.artifacts_metadata = metadata
- build.artifacts_expire_in =
- params['expire_in'] ||
- Gitlab::CurrentSettings.current_application_settings
- .default_artifacts_expire_in
-
- if build.save
- present(build, with: Entities::BuildDetails)
- else
- render_validation_error!(build)
- end
- end
-
- # Download the artifacts file from build - Runners only
- #
- # Parameters:
- # id (required) - The ID of a build
- # token (required) - The build authorization token
- # Headers:
- # BUILD-TOKEN (required) - The build authorization token, the same as token
- # Example Request:
- # GET /builds/:id/artifacts
- get ":id/artifacts" do
- build = authenticate_build!
- artifacts_file = build.artifacts_file
-
- unless artifacts_file.exists?
- not_found!
- end
-
- unless artifacts_file.file_storage?
- return redirect(build.artifacts_file.url)
- end
-
- present_file!(artifacts_file.path, artifacts_file.filename)
- end
-
- # Remove the artifacts file from build - Runners only
- #
- # Parameters:
- # id (required) - The ID of a build
- # token (required) - The build authorization token
- # Headers:
- # BUILD-TOKEN (required) - The build authorization token, the same as token
- # Example Request:
- # DELETE /builds/:id/artifacts
- delete ":id/artifacts" do
- build = authenticate_build!
-
- status(200)
- build.erase_artifacts!
- end
- end
- end
- end
-end
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
deleted file mode 100644
index 31f66dd5a58..00000000000
--- a/lib/ci/api/entities.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-module Ci
- module API
- module Entities
- class Commit < Grape::Entity
- expose :id, :sha, :project_id, :created_at
- expose :status, :finished_at, :duration
- expose :git_commit_message, :git_author_name, :git_author_email
- end
-
- class CommitWithBuilds < Commit
- expose :builds
- end
-
- class ArtifactFile < Grape::Entity
- expose :filename, :size
- end
-
- class BuildOptions < Grape::Entity
- expose :image
- expose :services
- expose :artifacts
- expose :cache
- expose :dependencies
- expose :after_script
- end
-
- class Build < Grape::Entity
- expose :id, :ref, :tag, :sha, :status
- expose :name, :token, :stage
- expose :project_id
- expose :project_name
- expose :artifacts_file, using: ArtifactFile, if: ->(build, _) { build.artifacts? }
- end
-
- class BuildCredentials < Grape::Entity
- expose :type, :url, :username, :password
- end
-
- class BuildDetails < Build
- expose :commands
- expose :repo_url
- expose :before_sha
- expose :allow_git_fetch
- expose :token
- expose :artifacts_expire_at, if: ->(build, _) { build.artifacts? }
-
- expose :options do |model|
- # This part ensures that output of old API is still the same after adding support
- # for extended docker configuration options, used by new API
- #
- # I'm leaving this here, not in the model, because it should be removed at the same time
- # when old API will be removed (planned for August 2017).
- model.options.dup.tap do |options|
- options[:image] = options[:image][:name] if options[:image].is_a?(Hash)
- options[:services]&.map! do |service|
- if service.is_a?(Hash)
- service[:name]
- else
- service
- end
- end
- end
- end
-
- expose :timeout do |model|
- model.timeout
- end
-
- expose :variables
- expose :depends_on_builds, using: Build
-
- expose :credentials, using: BuildCredentials
- end
-
- class Runner < Grape::Entity
- expose :id, :token
- end
-
- class RunnerProject < Grape::Entity
- expose :id, :project_id, :runner_id
- end
-
- class WebHook < Grape::Entity
- expose :id, :project_id, :url
- end
-
- class TriggerRequest < Grape::Entity
- expose :id, :variables
- expose :pipeline, using: Commit, as: :commit
- end
- end
- end
-end
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
deleted file mode 100644
index a40b6ab6c9f..00000000000
--- a/lib/ci/api/helpers.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-module Ci
- module API
- module Helpers
- BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN".freeze
- BUILD_TOKEN_PARAM = :token
- UPDATE_RUNNER_EVERY = 10 * 60
-
- def authenticate_runners!
- forbidden! unless runner_registration_token_valid?
- end
-
- def authenticate_runner!
- forbidden! unless current_runner
- end
-
- def authenticate_build!
- build = Ci::Build.find_by_id(params[:id])
-
- validate_build!(build) do
- forbidden! unless build_token_valid?(build)
- end
-
- build
- end
-
- def validate_build!(build)
- not_found! unless build
-
- yield if block_given?
-
- project = build.project
- forbidden!('Project has been deleted!') if project.nil? || project.pending_delete?
- forbidden!('Build has been erased!') if build.erased?
- end
-
- def runner_registration_token_valid?
- ActiveSupport::SecurityUtils.variable_size_secure_compare(
- params[:token],
- current_application_settings.runners_registration_token)
- end
-
- def build_token_valid?(build)
- token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s
-
- # We require to also check `runners_token` to maintain compatibility with old version of runners
- token && (build.valid_token?(token) || build.project.valid_runners_token?(token))
- end
-
- def update_runner_info
- return unless update_runner?
-
- current_runner.contacted_at = Time.now
- current_runner.assign_attributes(get_runner_version_from_params)
- current_runner.save if current_runner.changed?
- end
-
- def update_runner?
- # Use a random threshold to prevent beating DB updates.
- # It generates a distribution between [40m, 80m].
- #
- contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY)
-
- current_runner.contacted_at.nil? ||
- (Time.now - current_runner.contacted_at) >= contacted_at_max_age
- end
-
- def build_not_found!
- if headers['User-Agent'].to_s =~ /gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /
- no_content!
- else
- not_found!
- end
- end
-
- def current_runner
- @runner ||= Runner.find_by_token(params[:token].to_s)
- end
-
- def get_runner_version_from_params
- return unless params["info"].present?
- attributes_for_keys(%w(name version revision platform architecture), params["info"])
- end
-
- def max_artifacts_size
- current_application_settings.max_artifacts_size.megabytes.to_i
- end
- end
- end
-end
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
deleted file mode 100644
index 45aa2adccf5..00000000000
--- a/lib/ci/api/runners.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module Ci
- module API
- class Runners < Grape::API
- resource :runners do
- desc 'Delete a runner'
- params do
- requires :token, type: String, desc: 'The unique token of the runner'
- end
- delete "delete" do
- authenticate_runner!
-
- status(200)
- Ci::Runner.find_by_token(params[:token]).destroy
- end
-
- desc 'Register a new runner' do
- success Entities::Runner
- end
- params do
- requires :token, type: String, desc: 'The unique token of the runner'
- optional :description, type: String, desc: 'The description of the runner'
- optional :tag_list, type: Array[String], desc: 'A list of tags the runner should run for'
- optional :run_untagged, type: Boolean, desc: 'Flag if the runner should execute untagged jobs'
- optional :locked, type: Boolean, desc: 'Lock this runner for this specific project'
- end
- post "register" do
- runner_params = declared(params, include_missing: false).except(:token)
-
- runner =
- if runner_registration_token_valid?
- # Create shared runner. Requires admin access
- Ci::Runner.create(runner_params.merge(is_shared: true))
- elsif project = Project.find_by(runners_token: params[:token])
- # Create a specific runner for project.
- project.runners.create(runner_params)
- end
-
- return forbidden! unless runner
-
- if runner.id
- runner.update(get_runner_version_from_params)
- present runner, with: Entities::Runner
- else
- not_found!
- end
- end
- end
- end
- end
-end
diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb
deleted file mode 100644
index 6225203f223..00000000000
--- a/lib/ci/api/triggers.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-module Ci
- module API
- class Triggers < Grape::API
- resource :projects do
- desc 'Trigger a GitLab CI project build' do
- success Entities::TriggerRequest
- end
- params do
- requires :id, type: Integer, desc: 'The ID of a CI project'
- requires :ref, type: String, desc: "The name of project's branch or tag"
- requires :token, type: String, desc: 'The unique token of the trigger'
- optional :variables, type: Hash, desc: 'Optional build variables'
- end
- post ":id/refs/:ref/trigger" do
- project = Project.find_by(ci_id: params[:id])
- trigger = Ci::Trigger.find_by_token(params[:token])
- not_found! unless project && trigger
- unauthorized! unless trigger.project == project
-
- # Validate variables
- variables = params[:variables].to_h
- unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
- render_api_error!('variables needs to be a map of key-valued strings', 400)
- end
-
- # create request and trigger builds
- result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref], variables)
- pipeline = result.pipeline
-
- if pipeline.persisted?
- present result.trigger_request, with: Entities::TriggerRequest
- else
- render_validation_error!(pipeline)
- end
- end
- end
- end
- end
-end
diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb
index 872e418c788..76a69bf8a83 100644
--- a/lib/ci/charts.rb
+++ b/lib/ci/charts.rb
@@ -47,7 +47,7 @@ module Ci
def collect
query = project.pipelines
- .where("? > #{Ci::Pipeline.table_name}.created_at AND #{Ci::Pipeline.table_name}.created_at > ?", @to, @from)
+ .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)
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 3a4911b23b0..62b44389b15 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -20,24 +20,6 @@ module Ci
raise ValidationError, e.message
end
- def jobs_for_ref(ref, tag = false, source = nil)
- @jobs.select do |_, job|
- process?(job[:only], job[:except], 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 builds_for_ref(ref, tag = false, source = nil)
- jobs_for_ref(ref, tag, source).map do |name, _|
- build_attributes(name)
- end
- 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)
@@ -52,8 +34,7 @@ module Ci
def stage_seeds(pipeline)
seeds = @stages.uniq.map do |stage|
- builds = builds_for_stage_and_ref(
- stage, pipeline.ref, pipeline.tag?, pipeline.source)
+ builds = pipeline_stage_builds(stage, pipeline)
Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
end
@@ -101,6 +82,34 @@ module Ci
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
diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb
index 4c0aee6c48f..5bef29eb1da 100644
--- a/lib/constraints/project_url_constrainer.rb
+++ b/lib/constraints/project_url_constrainer.rb
@@ -6,6 +6,8 @@ class ProjectUrlConstrainer
return false unless DynamicPathValidator.valid_project_path?(full_path)
+ # We intentionally allow SELECT(*) here so result of this query can be used
+ # as cache for further Project.find_by_full_path calls within request
Project.find_by_full_path(full_path, follow_redirects: request.get?).present?
end
end
diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb
index df94cafb6a1..b028169f500 100644
--- a/lib/declarative_policy/base.rb
+++ b/lib/declarative_policy/base.rb
@@ -109,7 +109,7 @@ module DeclarativePolicy
name = name.to_sym
if delegation_block.nil?
- delegation_block = proc { @subject.__send__(name) }
+ delegation_block = proc { @subject.__send__(name) } # rubocop:disable GitlabSecurity/PublicSend
end
own_delegations[name] = delegation_block
@@ -221,7 +221,7 @@ module DeclarativePolicy
end
# computes the given ability and prints a helpful debugging output
- # showing which
+ # showing which
def debug(ability, *a)
runner(ability).debug(*a)
end
diff --git a/lib/declarative_policy/dsl.rb b/lib/declarative_policy/dsl.rb
index b26807a7622..6ba1e7a3c5c 100644
--- a/lib/declarative_policy/dsl.rb
+++ b/lib/declarative_policy/dsl.rb
@@ -93,7 +93,7 @@ module DeclarativePolicy
def method_missing(m, *a, &b)
return super unless @context_class.respond_to?(m)
- @context_class.__send__(m, *a, &b)
+ @context_class.__send__(m, *a, &b) # rubocop:disable GitlabSecurity/PublicSend
end
def respond_to_missing?(m)
diff --git a/lib/email_template_interceptor.rb b/lib/email_template_interceptor.rb
index 63f9f8d7a5a..f2bf3d0fb2b 100644
--- a/lib/email_template_interceptor.rb
+++ b/lib/email_template_interceptor.rb
@@ -1,6 +1,6 @@
# Read about interceptors in http://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails
class EmailTemplateInterceptor
- include Gitlab::CurrentSettings
+ extend Gitlab::CurrentSettings
def self.delivering_email(message)
# Remove HTML part if HTML emails are disabled.
diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb
index eb19ab45ac3..de391de9059 100644
--- a/lib/file_size_validator.rb
+++ b/lib/file_size_validator.rb
@@ -44,13 +44,13 @@ class FileSizeValidator < ActiveModel::EachValidator
when Integer
check_value
when Symbol
- record.send(check_value)
+ record.public_send(check_value) # rubocop:disable GitlabSecurity/PublicSend
end
value ||= [] if key == :maximum
value_size = value.size
- next if value_size.send(validity_check, check_value)
+ next if value_size.public_send(validity_check, check_value) # rubocop:disable GitlabSecurity/PublicSend
errors_options = options.except(*RESERVED_OPTIONS)
errors_options[:file_size] = help.number_to_human_size check_value
diff --git a/lib/file_streamer.rb b/lib/file_streamer.rb
deleted file mode 100644
index 4e3c6d3c773..00000000000
--- a/lib/file_streamer.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-class FileStreamer #:nodoc:
- attr_reader :to_path
-
- def initialize(path)
- @to_path = path
- end
-
- # Stream the file's contents if Rack::Sendfile isn't present.
- def each
- File.open(to_path, 'rb') do |file|
- while chunk = file.read(16384)
- yield chunk
- end
- end
- end
-end
diff --git a/lib/github/import.rb b/lib/github/import.rb
index 4cc01593ef4..9354e142d3d 100644
--- a/lib/github/import.rb
+++ b/lib/github/import.rb
@@ -107,7 +107,7 @@ module Github
# this means that repo has wiki enabled, but have no pages. So,
# we can skip the import.
if e.message !~ /repository not exported/
- errors(:wiki, wiki_url, e.message)
+ error(:wiki, wiki_url, e.message)
end
end
@@ -226,49 +226,51 @@ module Github
while url
response = Github::Client.new(options).get(url, state: :all, sort: :created, direction: :asc)
- response.body.each do |raw|
- representation = Github::Representation::Issue.new(raw, options)
+ response.body.each { |raw| populate_issue(raw) }
- begin
- # Every pull request is an issue, but not every issue
- # is a pull request. For this reason, "shared" actions
- # for both features, like manipulating assignees, labels
- # and milestones, are provided within the Issues API.
- if representation.pull_request?
- next unless representation.has_labels?
-
- merge_request = MergeRequest.find_by!(target_project_id: project.id, iid: representation.iid)
- merge_request.update_attribute(:label_ids, label_ids(representation.labels))
- else
- next if Issue.where(iid: representation.iid, project_id: project.id).exists?
-
- author_id = user_id(representation.author, project.creator_id)
- issue = Issue.new
- issue.iid = representation.iid
- issue.project_id = project.id
- issue.title = representation.title
- issue.description = format_description(representation.description, representation.author)
- issue.state = representation.state
- issue.label_ids = label_ids(representation.labels)
- issue.milestone_id = milestone_id(representation.milestone)
- issue.author_id = author_id
- issue.assignee_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
- end
- rescue => e
- error(:issue, representation.url, e.message)
+ url = response.rels[:next]
+ end
+ end
+
+ def populate_issue(raw)
+ representation = Github::Representation::Issue.new(raw, options)
+
+ begin
+ # Every pull request is an issue, but not every issue
+ # is a pull request. For this reason, "shared" actions
+ # for both features, like manipulating assignees, labels
+ # and milestones, are provided within the Issues API.
+ if representation.pull_request?
+ return unless representation.has_labels?
+
+ merge_request = MergeRequest.find_by!(target_project_id: project.id, iid: representation.iid)
+ merge_request.update_attribute(:label_ids, label_ids(representation.labels))
+ else
+ return if Issue.where(iid: representation.iid, project_id: project.id).exists?
+
+ author_id = user_id(representation.author, project.creator_id)
+ issue = Issue.new
+ issue.iid = representation.iid
+ issue.project_id = project.id
+ issue.title = representation.title
+ issue.description = format_description(representation.description, representation.author)
+ issue.state = representation.state
+ issue.label_ids = label_ids(representation.labels)
+ issue.milestone_id = milestone_id(representation.milestone)
+ issue.author_id = author_id
+ issue.assignee_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
end
-
- url = response.rels[:next]
+ rescue => e
+ error(:issue, representation.url, e.message)
end
end
diff --git a/lib/github/representation/branch.rb b/lib/github/representation/branch.rb
index c6fa928d565..823e8e9a9c4 100644
--- a/lib/github/representation/branch.rb
+++ b/lib/github/representation/branch.rb
@@ -41,7 +41,7 @@ module Github
def remove!(name)
repository.delete_branch(name)
- rescue Rugged::ReferenceError => e
+ rescue Gitlab::Git::Repository::DeleteBranchError => e
Rails.logger.error("#{self.class.name}: Could not remove branch #{name}: #{e}")
end
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 4714ab18cc1..b4012ebbb99 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -67,10 +67,14 @@ module Gitlab
def protection_values
protection_options.values
end
+
+ def human_access(access)
+ options_with_owner.key(access)
+ end
end
def human_access
- Gitlab::Access.options_with_owner.key(access_field)
+ Gitlab::Access.human_access(access_field)
end
def owner?
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index 3d41ac76406..cead1c7eacd 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -6,6 +6,8 @@ module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
# the resulting HTML through HTML pipeline filters.
module Asciidoc
+ extend Gitlab::CurrentSettings
+
DEFAULT_ADOC_ATTRS = [
'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab',
'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font'
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 7d3aa532750..3fd81759d25 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -19,6 +19,8 @@ module Gitlab
OPTIONAL_SCOPES = (AVAILABLE_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze
class << self
+ include Gitlab::CurrentSettings
+
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
@@ -48,10 +50,6 @@ module Gitlab
# Avoid resource intensive login checks if password is not provided
return unless password.present?
- # Nothing to do here if internal auth is disabled and LDAP is
- # not configured
- return unless current_application_settings.password_authentication_enabled? || Gitlab::LDAP::Config.enabled?
-
Gitlab::Auth::UniqueIpsLimiter.limit_user! do
user = User.by_login(login)
@@ -101,7 +99,7 @@ module Gitlab
if Service.available_services_names.include?(underscored_service)
# We treat underscored_service as a trusted input because it is included
# in the Service.available_services_names whitelist.
- service = project.public_send("#{underscored_service}_service")
+ service = project.public_send("#{underscored_service}_service") # rubocop:disable GitlabSecurity/PublicSend
if service && service.activated? && service.valid_token?(password)
Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)
@@ -149,7 +147,7 @@ module Gitlab
def abilities_for_scope(scopes)
scopes.map do |scope|
- self.public_send(:"#{scope}_scope_authentication_abilities")
+ self.public_send(:"#{scope}_scope_authentication_abilities") # rubocop:disable GitlabSecurity/PublicSend
end.flatten.uniq
end
diff --git a/lib/gitlab/auth/ip_rate_limiter.rb b/lib/gitlab/auth/ip_rate_limiter.rb
index 1089bc9f89e..e6173d45af3 100644
--- a/lib/gitlab/auth/ip_rate_limiter.rb
+++ b/lib/gitlab/auth/ip_rate_limiter.rb
@@ -11,11 +11,11 @@ module Gitlab
def enabled?
config.enabled
end
-
+
def reset!
Rack::Attack::Allow2Ban.reset(ip, config)
end
-
+
def register_fail!
# Allow2Ban.filter will return false if this IP has not failed too often yet
@banned = Rack::Attack::Allow2Ban.filter(ip, config) do
@@ -23,17 +23,17 @@ module Gitlab
ip_can_be_banned?
end
end
-
+
def banned?
@banned
end
-
+
private
-
+
def config
Gitlab.config.rack_attack.git_basic_auth
end
-
+
def ip_can_be_banned?
config.ip_whitelist.exclude?(ip)
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 0fbc6b70989..3fde1b09efb 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
@@ -81,10 +81,15 @@ module Gitlab
relative_order: index
)
- # Compatibility with old diffs created with Psych.
diff_hash.tap do |hash|
diff_text = hash[:diff]
+ hash[:too_large] = !!hash[:too_large]
+
+ hash[:a_mode] ||= guess_mode(hash[:new_file], hash[:diff])
+ hash[:b_mode] ||= guess_mode(hash[:deleted_file], hash[:diff])
+
+ # Compatibility with old diffs created with Psych.
if diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only?
hash[:binary] = true
hash[:diff] = [diff_text].pack('m0')
@@ -95,6 +100,15 @@ module Gitlab
[commit_rows, file_rows]
end
+ # This doesn't have to be 100% accurate, because it's only used for
+ # display - it won't change file modes in the repository. Submodules are
+ # created as 600, regular files as 644.
+ def guess_mode(file_missing, diff)
+ return '0' if file_missing
+
+ diff.include?('Subproject commit') ? '160000' : '100644'
+ end
+
# Unlike MergeRequestDiff#valid_raw_diff?, don't count Rugged objects as
# valid, because we don't render them usefully anyway.
def valid_raw_diffs?(diffs)
diff --git a/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb b/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb
new file mode 100644
index 00000000000..432f7c3e706
--- /dev/null
+++ b/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb
@@ -0,0 +1,176 @@
+module Gitlab
+ module BackgroundMigration
+ # Class that migrates events for the new push event payloads setup. All
+ # events are copied to a shadow table, and push events will also have a row
+ # created in the push_event_payloads table.
+ class MigrateEventsToPushEventPayloads
+ class Event < ActiveRecord::Base
+ self.table_name = 'events'
+
+ serialize :data
+
+ BLANK_REF = ('0' * 40).freeze
+ TAG_REF_PREFIX = 'refs/tags/'.freeze
+ MAX_INDEX = 69
+ PUSHED = 5
+
+ def push_event?
+ action == PUSHED && data.present?
+ end
+
+ def commit_title
+ commit = commits.last
+
+ return nil unless commit && commit[:message]
+
+ index = commit[:message].index("\n")
+ message = index ? commit[:message][0..index] : commit[:message]
+
+ message.strip.truncate(70)
+ end
+
+ def commit_from_sha
+ if create?
+ nil
+ else
+ data[:before]
+ end
+ end
+
+ def commit_to_sha
+ if remove?
+ nil
+ else
+ data[:after]
+ end
+ end
+
+ def data
+ super || {}
+ end
+
+ def commits
+ data[:commits] || []
+ end
+
+ def commit_count
+ data[:total_commits_count] || 0
+ end
+
+ def ref
+ data[:ref]
+ end
+
+ def trimmed_ref_name
+ if ref_type == :tag
+ ref[10..-1]
+ else
+ ref[11..-1]
+ end
+ end
+
+ def create?
+ data[:before] == BLANK_REF
+ end
+
+ def remove?
+ data[:after] == BLANK_REF
+ end
+
+ def push_action
+ if create?
+ :created
+ elsif remove?
+ :removed
+ else
+ :pushed
+ end
+ end
+
+ def ref_type
+ if ref.start_with?(TAG_REF_PREFIX)
+ :tag
+ else
+ :branch
+ end
+ end
+ end
+
+ class EventForMigration < ActiveRecord::Base
+ self.table_name = 'events_for_migration'
+ end
+
+ class PushEventPayload < ActiveRecord::Base
+ self.table_name = 'push_event_payloads'
+
+ enum action: {
+ created: 0,
+ removed: 1,
+ pushed: 2
+ }
+
+ enum ref_type: {
+ branch: 0,
+ tag: 1
+ }
+ 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?
+
+ find_events(start_id, end_id).each { |event| process_event(event) }
+ end
+
+ def process_event(event)
+ replicate_event(event)
+ create_push_event_payload(event) if event.push_event?
+ end
+
+ def replicate_event(event)
+ new_attributes = event.attributes
+ .with_indifferent_access.except(:title, :data)
+
+ EventForMigration.create!(new_attributes)
+ rescue ActiveRecord::InvalidForeignKey
+ # A foreign key error means the associated event was removed. In this
+ # case we'll just skip migrating the event.
+ end
+
+ def create_push_event_payload(event)
+ commit_from = pack(event.commit_from_sha)
+ commit_to = pack(event.commit_to_sha)
+
+ PushEventPayload.create!(
+ event_id: event.id,
+ commit_count: event.commit_count,
+ ref_type: event.ref_type,
+ action: event.push_action,
+ commit_from: commit_from,
+ commit_to: commit_to,
+ ref: event.trimmed_ref_name,
+ commit_title: event.commit_title
+ )
+ rescue ActiveRecord::InvalidForeignKey
+ # A foreign key error means the associated event was removed. In this
+ # case we'll just skip migrating the event.
+ end
+
+ def find_events(start_id, end_id)
+ Event
+ .where('NOT EXISTS (SELECT true FROM events_for_migration WHERE events_for_migration.id = events.id)')
+ .where(id: start_id..end_id)
+ end
+
+ def migrate?
+ Event.table_exists? && PushEventPayload.table_exists? &&
+ EventForMigration.table_exists?
+ end
+
+ def pack(value)
+ value ? [value].pack('H*') : nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/migrate_stage_status.rb b/lib/gitlab/background_migration/migrate_stage_status.rb
new file mode 100644
index 00000000000..b1ff0900709
--- /dev/null
+++ b/lib/gitlab/background_migration/migrate_stage_status.rb
@@ -0,0 +1,77 @@
+module Gitlab
+ module BackgroundMigration
+ class MigrateStageStatus
+ STATUSES = { created: 0, pending: 1, running: 2, success: 3,
+ failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
+
+ class Build < ActiveRecord::Base
+ self.table_name = 'ci_builds'
+
+ scope :latest, -> { where(retried: [false, nil]) }
+ scope :created, -> { where(status: 'created') }
+ scope :running, -> { where(status: 'running') }
+ scope :pending, -> { where(status: 'pending') }
+ scope :success, -> { where(status: 'success') }
+ scope :failed, -> { where(status: 'failed') }
+ scope :canceled, -> { where(status: 'canceled') }
+ scope :skipped, -> { where(status: 'skipped') }
+ scope :manual, -> { where(status: 'manual') }
+
+ scope :failed_but_allowed, -> do
+ where(allow_failure: true, status: [:failed, :canceled])
+ end
+
+ scope :exclude_ignored, -> do
+ where("allow_failure = ? OR status IN (?)",
+ false, %w[created pending running success skipped])
+ end
+
+ def self.status_sql
+ scope_relevant = latest.exclude_ignored
+ scope_warnings = latest.failed_but_allowed
+
+ builds = scope_relevant.select('count(*)').to_sql
+ created = scope_relevant.created.select('count(*)').to_sql
+ success = scope_relevant.success.select('count(*)').to_sql
+ manual = scope_relevant.manual.select('count(*)').to_sql
+ pending = scope_relevant.pending.select('count(*)').to_sql
+ running = scope_relevant.running.select('count(*)').to_sql
+ skipped = scope_relevant.skipped.select('count(*)').to_sql
+ canceled = scope_relevant.canceled.select('count(*)').to_sql
+ warnings = scope_warnings.select('count(*) > 0').to_sql
+
+ <<-SQL.strip_heredoc
+ (CASE
+ WHEN (#{builds}) = (#{skipped}) AND (#{warnings}) THEN #{STATUSES[:success]}
+ WHEN (#{builds}) = (#{skipped}) THEN #{STATUSES[:skipped]}
+ WHEN (#{builds}) = (#{success}) THEN #{STATUSES[:success]}
+ WHEN (#{builds}) = (#{created}) THEN #{STATUSES[:created]}
+ WHEN (#{builds}) = (#{success}) + (#{skipped}) THEN #{STATUSES[:success]}
+ WHEN (#{builds}) = (#{success}) + (#{skipped}) + (#{canceled}) THEN #{STATUSES[:canceled]}
+ WHEN (#{builds}) = (#{created}) + (#{skipped}) + (#{pending}) THEN #{STATUSES[:pending]}
+ WHEN (#{running}) + (#{pending}) > 0 THEN #{STATUSES[:running]}
+ WHEN (#{manual}) > 0 THEN #{STATUSES[:manual]}
+ WHEN (#{created}) > 0 THEN #{STATUSES[:running]}
+ ELSE #{STATUSES[:failed]}
+ END)
+ SQL
+ end
+ end
+
+ def perform(start_id, stop_id)
+ status_sql = Build
+ .where('ci_builds.commit_id = ci_stages.pipeline_id')
+ .where('ci_builds.stage = ci_stages.name')
+ .status_sql
+
+ sql = <<-SQL
+ UPDATE ci_stages SET status = (#{status_sql})
+ WHERE ci_stages.status IS NULL
+ AND ci_stages.id BETWEEN #{start_id.to_i} AND #{stop_id.to_i}
+ SQL
+
+ ActiveRecord::Base.connection.execute(sql)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/move_personal_snippet_files.rb b/lib/gitlab/background_migration/move_personal_snippet_files.rb
new file mode 100644
index 00000000000..07cec96bcc3
--- /dev/null
+++ b/lib/gitlab/background_migration/move_personal_snippet_files.rb
@@ -0,0 +1,79 @@
+module Gitlab
+ module BackgroundMigration
+ class MovePersonalSnippetFiles
+ delegate :select_all, :execute, :quote_string, to: :connection
+
+ def perform(relative_source, relative_destination)
+ @source_relative_location = relative_source
+ @destination_relative_location = relative_destination
+
+ move_personal_snippet_files
+ end
+
+ def move_personal_snippet_files
+ query = "SELECT uploads.path, uploads.model_id FROM uploads "\
+ "INNER JOIN snippets ON snippets.id = uploads.model_id WHERE uploader = 'PersonalFileUploader'"
+ select_all(query).each do |upload|
+ secret = upload['path'].split('/')[0]
+ file_name = upload['path'].split('/')[1]
+
+ move_file(upload['model_id'], secret, file_name)
+ update_markdown(upload['model_id'], secret, file_name)
+ end
+ end
+
+ def move_file(snippet_id, secret, file_name)
+ source_dir = File.join(base_directory, @source_relative_location, snippet_id.to_s, secret)
+ destination_dir = File.join(base_directory, @destination_relative_location, snippet_id.to_s, secret)
+
+ source_file_path = File.join(source_dir, file_name)
+ destination_file_path = File.join(destination_dir, file_name)
+
+ unless File.exist?(source_file_path)
+ say "Source file `#{source_file_path}` doesn't exist. Skipping."
+ return
+ end
+
+ say "Moving file #{source_file_path} -> #{destination_file_path}"
+
+ FileUtils.mkdir_p(destination_dir)
+ FileUtils.move(source_file_path, destination_file_path)
+ end
+
+ def update_markdown(snippet_id, secret, file_name)
+ source_markdown_path = File.join(@source_relative_location, snippet_id.to_s, secret, file_name)
+ destination_markdown_path = File.join(@destination_relative_location, snippet_id.to_s, secret, file_name)
+
+ source_markdown = "](#{source_markdown_path})"
+ destination_markdown = "](#{destination_markdown_path})"
+ quoted_source = quote_string(source_markdown)
+ quoted_destination = quote_string(destination_markdown)
+
+ execute("UPDATE snippets "\
+ "SET description = replace(snippets.description, '#{quoted_source}', '#{quoted_destination}'), description_html = NULL "\
+ "WHERE id = #{snippet_id}")
+
+ query = "SELECT id, note FROM notes WHERE noteable_id = #{snippet_id} "\
+ "AND noteable_type = 'Snippet' AND note IS NOT NULL"
+ select_all(query).each do |note|
+ text = note['note'].gsub(source_markdown, destination_markdown)
+ quoted_text = quote_string(text)
+
+ execute("UPDATE notes SET note = '#{quoted_text}', note_html = NULL WHERE id = #{note['id']}")
+ end
+ end
+
+ def base_directory
+ File.join(Rails.root, 'public')
+ end
+
+ def connection
+ ActiveRecord::Base.connection
+ end
+
+ def say(message)
+ Rails.logger.debug(message)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/bare_repository_importer.rb b/lib/gitlab/bare_repository_importer.rb
new file mode 100644
index 00000000000..9323bfc7fb2
--- /dev/null
+++ b/lib/gitlab/bare_repository_importer.rb
@@ -0,0 +1,96 @@
+module Gitlab
+ class BareRepositoryImporter
+ NoAdminError = Class.new(StandardError)
+
+ def self.execute
+ Gitlab.config.repositories.storages.each do |storage_name, repository_storage|
+ git_base_path = repository_storage['path']
+ repos_to_import = Dir.glob(git_base_path + '/**/*.git')
+
+ repos_to_import.each do |repo_path|
+ if repo_path.end_with?('.wiki.git')
+ log " * Skipping wiki repo"
+ next
+ end
+
+ log "Processing #{repo_path}".color(:yellow)
+
+ repo_relative_path = repo_path[repository_storage['path'].length..-1]
+ .sub(/^\//, '') # Remove leading `/`
+ .sub(/\.git$/, '') # Remove `.git` at the end
+ new(storage_name, repo_relative_path).create_project_if_needed
+ end
+ end
+ end
+
+ attr_reader :storage_name, :full_path, :group_path, :project_path, :user
+ delegate :log, to: :class
+
+ def initialize(storage_name, repo_path)
+ @storage_name = storage_name
+ @full_path = repo_path
+
+ unless @user = User.admins.order_id_asc.first
+ raise NoAdminError.new('No admin user found to import repositories')
+ end
+
+ @group_path, @project_path = File.split(repo_path)
+ @group_path = nil if @group_path == '.'
+ end
+
+ def create_project_if_needed
+ if project = Project.find_by_full_path(full_path)
+ log " * #{project.name} (#{full_path}) exists"
+ return project
+ end
+
+ create_project
+ end
+
+ private
+
+ def create_project
+ group = find_or_create_group
+
+ project_params = {
+ name: project_path,
+ path: project_path,
+ repository_storage: storage_name,
+ namespace_id: group&.id
+ }
+
+ project = Projects::CreateService.new(user, project_params).execute
+
+ if project.persisted?
+ log " * Created #{project.name} (#{full_path})".color(:green)
+ ProjectCacheWorker.perform_async(project.id)
+ else
+ log " * Failed trying to create #{project.name} (#{full_path})".color(:red)
+ log " Errors: #{project.errors.messages}".color(:red)
+ end
+
+ project
+ end
+
+ def find_or_create_group
+ return nil unless group_path
+
+ if namespace = Namespace.find_by_full_path(group_path)
+ log " * Namespace #{group_path} exists.".color(:green)
+ return namespace
+ end
+
+ log " * Creating Group: #{group_path}"
+ Groups::NestedCreateService.new(user, group_path: group_path).execute
+ end
+
+ # This is called from within a rake task only used by Admins, so allow writing
+ # to STDOUT
+ #
+ # rubocop:disable Rails/Output
+ def self.log(message)
+ puts message
+ end
+ # rubocop:enable Rails/Output
+ end
+end
diff --git a/lib/gitlab/cache/request_cache.rb b/lib/gitlab/cache/request_cache.rb
index f1a04affd38..754a45c3257 100644
--- a/lib/gitlab/cache/request_cache.rb
+++ b/lib/gitlab/cache/request_cache.rb
@@ -69,7 +69,7 @@ module Gitlab
instance_variable_set(ivar_name, {})
end
- key = __send__(cache_key_method_name, args)
+ key = __send__(cache_key_method_name, args) # rubocop:disable GitlabSecurity/PublicSend
store.fetch(key) { store[key] = super(*args) }
end
diff --git a/lib/gitlab/checks/force_push.rb b/lib/gitlab/checks/force_push.rb
index 1e73f89158d..dc5d285ea65 100644
--- a/lib/gitlab/checks/force_push.rb
+++ b/lib/gitlab/checks/force_push.rb
@@ -5,12 +5,19 @@ module Gitlab
return false if project.empty_repo?
# Created or deleted branch
- if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
- false
- else
- Gitlab::Git::RevList.new(
- path_to_repo: project.repository.path_to_repo,
- oldrev: oldrev, newrev: newrev).missed_ref.present?
+ return false if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
+
+ GitalyClient.migrate(:force_push) do |is_enabled|
+ if is_enabled
+ !project
+ .repository
+ .gitaly_commit_client
+ .ancestor?(oldrev, newrev)
+ else
+ Gitlab::Git::RevList.new(
+ path_to_repo: project.repository.path_to_repo,
+ oldrev: oldrev, newrev: newrev).missed_ref.present?
+ end
end
end
end
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index a375ccbece0..a788fb3fcbc 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -60,7 +60,7 @@ module Gitlab
begin
path = read_string(gz).force_encoding('UTF-8')
meta = read_string(gz).force_encoding('UTF-8')
-
+
next unless path.valid_encoding? && meta.valid_encoding?
next unless path =~ match_pattern
next if path =~ INVALID_PATH_PATTERN
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
index 2e073334abc..22941d48edf 100644
--- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -1,129 +1,129 @@
module Gitlab
- module Ci::Build::Artifacts
- class Metadata
- ##
- # Class that represents an entry (path and metadata) to a file or
- # directory in GitLab CI Build Artifacts binary file / archive
- #
- # This is IO-operations safe class, that does similar job to
- # Ruby's Pathname but without the risk of accessing filesystem.
- #
- # This class is working only with UTF-8 encoded paths.
- #
- class Entry
- attr_reader :path, :entries
- attr_accessor :name
-
- def initialize(path, entries)
- @path = path.dup.force_encoding('UTF-8')
- @entries = entries
-
- if path.include?("\0")
- raise ArgumentError, 'Path contains zero byte character!'
- end
+ module Ci
+ module Build
+ module Artifacts
+ class Metadata
+ ##
+ # Class that represents an entry (path and metadata) to a file or
+ # directory in GitLab CI Build Artifacts binary file / archive
+ #
+ # This is IO-operations safe class, that does similar job to
+ # Ruby's Pathname but without the risk of accessing filesystem.
+ #
+ # This class is working only with UTF-8 encoded paths.
+ #
+ class Entry
+ attr_reader :entries
+ attr_accessor :name
+
+ def initialize(path, entries)
+ @entries = entries
+ @path = Artifacts::Path.new(path)
+ end
+
+ delegate :empty?, to: :children
+
+ def directory?
+ blank_node? || @path.directory?
+ end
+
+ def file?
+ !directory?
+ end
+
+ def blob
+ return unless file?
+
+ @blob ||= Blob.decorate(::Ci::ArtifactBlob.new(self), nil)
+ end
+
+ def has_parent?
+ nodes > 0
+ end
+
+ def parent
+ return nil unless has_parent?
+ self.class.new(@path.to_s.chomp(basename), @entries)
+ end
+
+ def basename
+ (directory? && !blank_node?) ? name + '/' : name
+ end
+
+ def name
+ @name || @path.name
+ end
+
+ def children
+ return [] unless directory?
+ return @children if @children
+
+ child_pattern = %r{^#{Regexp.escape(@path.to_s)}[^/]+/?$}
+ @children = select_entries { |path| path =~ child_pattern }
+ end
+
+ def directories(opts = {})
+ return [] unless directory?
+ dirs = children.select(&:directory?)
+ return dirs unless has_parent? && opts[:parent]
+
+ dotted_parent = parent
+ dotted_parent.name = '..'
+ dirs.prepend(dotted_parent)
+ end
+
+ def files
+ return [] unless directory?
+ children.select(&:file?)
+ end
+
+ def metadata
+ @entries[@path.to_s] || {}
+ end
+
+ def nodes
+ @path.nodes + (file? ? 1 : 0)
+ end
+
+ def blank_node?
+ @path.to_s.empty? # "" is considered to be './'
+ end
+
+ def exists?
+ blank_node? || @entries.include?(@path.to_s)
+ end
+
+ def total_size
+ descendant_pattern = %r{^#{Regexp.escape(@path.to_s)}}
+ entries.sum do |path, entry|
+ (entry[:size] if path =~ descendant_pattern).to_i
+ end
+ end
+
+ def path
+ @path.to_s
+ end
+
+ def to_s
+ @path.to_s
+ end
+
+ def ==(other)
+ path == other.path && @entries == other.entries
+ end
+
+ def inspect
+ "#{self.class.name}: #{self}"
+ end
- unless path.valid_encoding?
- raise ArgumentError, 'Path contains non-UTF-8 byte sequence!'
+ private
+
+ def select_entries
+ selected = @entries.select { |path, _metadata| yield path }
+ selected.map { |path, _metadata| self.class.new(path, @entries) }
+ end
end
end
-
- delegate :empty?, to: :children
-
- def directory?
- blank_node? || @path.end_with?('/')
- end
-
- def file?
- !directory?
- end
-
- def blob
- return unless file?
-
- @blob ||= Blob.decorate(::Ci::ArtifactBlob.new(self), nil)
- end
-
- def has_parent?
- nodes > 0
- end
-
- def parent
- return nil unless has_parent?
- self.class.new(@path.chomp(basename), @entries)
- end
-
- def basename
- (directory? && !blank_node?) ? name + '/' : name
- end
-
- def name
- @name || @path.split('/').last.to_s
- end
-
- def children
- return [] unless directory?
- return @children if @children
-
- child_pattern = %r{^#{Regexp.escape(@path)}[^/]+/?$}
- @children = select_entries { |path| path =~ child_pattern }
- end
-
- def directories(opts = {})
- return [] unless directory?
- dirs = children.select(&:directory?)
- return dirs unless has_parent? && opts[:parent]
-
- dotted_parent = parent
- dotted_parent.name = '..'
- dirs.prepend(dotted_parent)
- end
-
- def files
- return [] unless directory?
- children.select(&:file?)
- end
-
- def metadata
- @entries[@path] || {}
- end
-
- def nodes
- @path.count('/') + (file? ? 1 : 0)
- end
-
- def blank_node?
- @path.empty? # "" is considered to be './'
- end
-
- def exists?
- blank_node? || @entries.include?(@path)
- end
-
- def total_size
- descendant_pattern = %r{^#{Regexp.escape(@path)}}
- entries.sum do |path, entry|
- (entry[:size] if path =~ descendant_pattern).to_i
- end
- end
-
- def to_s
- @path
- end
-
- def ==(other)
- @path == other.path && @entries == other.entries
- end
-
- def inspect
- "#{self.class.name}: #{@path}"
- end
-
- private
-
- def select_entries
- selected = @entries.select { |path, _metadata| yield path }
- selected.map { |path, _metadata| self.class.new(path, @entries) }
- end
end
end
end
diff --git a/lib/gitlab/ci/build/artifacts/path.rb b/lib/gitlab/ci/build/artifacts/path.rb
new file mode 100644
index 00000000000..9cd9b36c5f8
--- /dev/null
+++ b/lib/gitlab/ci/build/artifacts/path.rb
@@ -0,0 +1,51 @@
+module Gitlab
+ module Ci
+ module Build
+ module Artifacts
+ class Path
+ def initialize(path)
+ @path = path.dup.force_encoding('UTF-8')
+ end
+
+ def valid?
+ nonzero? && utf8?
+ end
+
+ def directory?
+ @path.end_with?('/')
+ end
+
+ def name
+ @path.split('/').last.to_s
+ end
+
+ def nodes
+ @path.count('/')
+ end
+
+ def to_s
+ @path.tap do |path|
+ unless nonzero?
+ raise ArgumentError, 'Path contains zero byte character!'
+ end
+
+ unless utf8?
+ raise ArgumentError, 'Path contains non-UTF-8 byte sequence!'
+ end
+ end
+ end
+
+ private
+
+ def nonzero?
+ @path.exclude?("\0")
+ end
+
+ def utf8?
+ @path.valid_encoding?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/attributable.rb b/lib/gitlab/ci/config/entry/attributable.rb
index 1c8b55ee4c4..3e87a09704e 100644
--- a/lib/gitlab/ci/config/entry/attributable.rb
+++ b/lib/gitlab/ci/config/entry/attributable.rb
@@ -8,6 +8,10 @@ module Gitlab
class_methods do
def attributes(*attributes)
attributes.flatten.each do |attribute|
+ if method_defined?(attribute)
+ raise ArgumentError, 'Method already defined!'
+ end
+
define_method(attribute) do
return unless config.is_a?(Hash)
diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
index e05aca9881b..68b6742385a 100644
--- a/lib/gitlab/ci/config/entry/configurable.rb
+++ b/lib/gitlab/ci/config/entry/configurable.rb
@@ -15,9 +15,10 @@ module Gitlab
#
module Configurable
extend ActiveSupport::Concern
- include Validatable
included do
+ include Validatable
+
validations do
validates :config, type: Hash
end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 32f5c6ab142..91aac6df4b1 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -59,10 +59,10 @@ module Gitlab
entry :services, Entry::Services,
description: 'Services that will be used to execute this job.'
- entry :only, Entry::Trigger,
+ entry :only, Entry::Policy,
description: 'Refs policy this job will be executed for.'
- entry :except, Entry::Trigger,
+ entry :except, Entry::Policy,
description: 'Refs policy this job will be executed for.'
entry :variables, Entry::Variables,
diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb
index a6a914d79c1..c868943c42e 100644
--- a/lib/gitlab/ci/config/entry/node.rb
+++ b/lib/gitlab/ci/config/entry/node.rb
@@ -16,8 +16,9 @@ module Gitlab
@metadata = metadata
@entries = {}
- @validator = self.class.validator.new(self)
- @validator.validate(:new)
+ self.class.aspects.to_a.each do |aspect|
+ instance_exec(&aspect)
+ end
end
def [](key)
@@ -47,7 +48,7 @@ module Gitlab
end
def errors
- @validator.messages + descendants.flat_map(&:errors)
+ []
end
def value
@@ -70,6 +71,13 @@ module Gitlab
true
end
+ def location
+ name = @key.presence || self.class.name.to_s.demodulize
+ .underscore.humanize.downcase
+
+ ancestors.map(&:key).append(name).compact.join(':')
+ end
+
def inspect
val = leaf? ? config : descendants
unspecified = specified? ? '' : '(unspecified) '
@@ -79,8 +87,8 @@ module Gitlab
def self.default
end
- def self.validator
- Validator
+ def self.aspects
+ @aspects ||= []
end
end
end
diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb
new file mode 100644
index 00000000000..0027e9ec8c5
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/policy.rb
@@ -0,0 +1,53 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents an only/except trigger policy for the job.
+ #
+ class Policy < Simplifiable
+ strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) }
+ strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) }
+
+ class RefsPolicy < Entry::Node
+ include Entry::Validatable
+
+ validations do
+ validates :config, array_of_strings_or_regexps: true
+ end
+
+ def value
+ { refs: @config }
+ end
+ end
+
+ class ComplexPolicy < Entry::Node
+ include Entry::Validatable
+ include Entry::Attributable
+
+ attributes :refs, :kubernetes
+
+ validations do
+ validates :config, presence: true
+ validates :config, allowed_keys: %i[refs kubernetes]
+
+ with_options allow_nil: true do
+ validates :refs, array_of_strings_or_regexps: true
+ validates :kubernetes, allowed_values: %w[active]
+ end
+ end
+ end
+
+ class UnknownStrategy < Entry::Node
+ def errors
+ ["#{location} has to be either an array of conditions or a hash"]
+ end
+ end
+
+ def self.default
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/simplifiable.rb b/lib/gitlab/ci/config/entry/simplifiable.rb
new file mode 100644
index 00000000000..12764629686
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/simplifiable.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Simplifiable < SimpleDelegator
+ EntryStrategy = Struct.new(:name, :condition)
+
+ def initialize(config, **metadata)
+ unless self.class.const_defined?(:UnknownStrategy)
+ raise ArgumentError, 'UndefinedStrategy not available!'
+ end
+
+ strategy = self.class.strategies.find do |variant|
+ variant.condition.call(config)
+ end
+
+ entry = self.class.entry_class(strategy)
+
+ super(entry.new(config, metadata))
+ end
+
+ def self.strategy(name, **opts)
+ EntryStrategy.new(name, opts.fetch(:if)).tap do |strategy|
+ strategies.append(strategy)
+ end
+ end
+
+ def self.strategies
+ @strategies ||= []
+ end
+
+ def self.entry_class(strategy)
+ if strategy.present?
+ self.const_get(strategy.name)
+ else
+ self::UnknownStrategy
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb
deleted file mode 100644
index 16b234e6c59..00000000000
--- a/lib/gitlab/ci/config/entry/trigger.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Entry
- ##
- # Entry that represents a trigger policy for the job.
- #
- class Trigger < Node
- include Validatable
-
- validations do
- validates :config, array_of_strings_or_regexps: true
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/entry/validatable.rb b/lib/gitlab/ci/config/entry/validatable.rb
index f7f1b111571..5ced778d311 100644
--- a/lib/gitlab/ci/config/entry/validatable.rb
+++ b/lib/gitlab/ci/config/entry/validatable.rb
@@ -5,6 +5,17 @@ module Gitlab
module Validatable
extend ActiveSupport::Concern
+ def self.included(node)
+ node.aspects.append -> do
+ @validator = self.class.validator.new(self)
+ @validator.validate(:new)
+ end
+ end
+
+ def errors
+ @validator.messages + descendants.flat_map(&:errors)
+ end
+
class_methods do
def validator
@validator ||= Class.new(Entry::Validator).tap do |validator|
diff --git a/lib/gitlab/ci/config/entry/validator.rb b/lib/gitlab/ci/config/entry/validator.rb
index 55343005fe3..2df23a3edcd 100644
--- a/lib/gitlab/ci/config/entry/validator.rb
+++ b/lib/gitlab/ci/config/entry/validator.rb
@@ -8,7 +8,6 @@ module Gitlab
def initialize(entry)
super(entry)
- @entry = entry
end
def messages
@@ -20,21 +19,6 @@ module Gitlab
def self.name
'Validator'
end
-
- private
-
- def location
- predecessors = ancestors.map(&:key).compact
- predecessors.append(key_name).join(':')
- end
-
- def key_name
- if key.blank?
- @entry.class.name.demodulize.underscore.humanize
- else
- key
- end
- end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index b2ca3c881e4..0159179f0a9 100644
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -14,6 +14,14 @@ module Gitlab
end
end
+ class AllowedValuesValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless options[:in].include?(value.to_s)
+ record.errors.add(attribute, "unknown value: #{value}")
+ end
+ end
+ end
+
class ArrayOfStringsValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
diff --git a/lib/gitlab/ci/stage/seed.rb b/lib/gitlab/ci/stage/seed.rb
index f81f9347b4d..e19aae35a81 100644
--- a/lib/gitlab/ci/stage/seed.rb
+++ b/lib/gitlab/ci/stage/seed.rb
@@ -28,7 +28,8 @@ module Gitlab
attributes.merge(project: project,
ref: pipeline.ref,
tag: pipeline.tag,
- trigger_request: trigger)
+ trigger_request: trigger,
+ protected: protected_ref?)
end
end
@@ -43,6 +44,12 @@ module Gitlab
end
end
end
+
+ private
+
+ def protected_ref?
+ @protected_ref ||= project.protected_for?(pipeline.ref)
+ end
end
end
end
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
index 1611eba31da..90f83e0f810 100644
--- a/lib/gitlab/conflict/file_collection.rb
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -18,7 +18,7 @@ module Gitlab
new(merge_request, project).tap do |file_collection|
project
.repository
- .with_repo_branch_commit(merge_request.target_project.repository, merge_request.target_branch) do
+ .with_repo_branch_commit(merge_request.target_project.repository.raw_repository, merge_request.target_branch) do
yield file_collection
end
@@ -77,8 +77,8 @@ EOM
def initialize(merge_request, project)
@merge_request = merge_request
- @our_commit = merge_request.source_branch_head.raw.raw_commit
- @their_commit = merge_request.target_branch_head.raw.raw_commit
+ @our_commit = merge_request.source_branch_head.raw.rugged_commit
+ @their_commit = merge_request.target_branch_head.raw.rugged_commit
@project = project
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 7fa02f3d7b3..642f0944354 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -1,5 +1,7 @@
module Gitlab
module CurrentSettings
+ extend self
+
def current_application_settings
if RequestStore.active?
RequestStore.fetch(:current_application_settings) { ensure_application_settings! }
diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index b260822788d..2479b4a7706 100644
--- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -54,7 +54,7 @@ module Gitlab
end
def serialize_commit(event, commit, query)
- commit = Commit.new(Gitlab::Git::Commit.new(commit.to_hash), @project)
+ commit = Commit.from_hash(commit.to_hash, @project)
AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit)
end
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 5c5f507d44d..4ab5b3455a5 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -3,6 +3,35 @@ module Gitlab
module Push
extend self
+ SAMPLE_DATA =
+ {
+ object_kind: "push",
+ event_name: "push",
+ before: "95790bf891e76fee5e1747ab589903a6a1f80f22",
+ after: "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ ref: "refs/heads/master",
+ checkout_sha: "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ message: "Hello World",
+ user_id: 4,
+ user_name: "John Smith",
+ user_email: "john@example.com",
+ user_avatar: "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ project_id: 15,
+ commits: [
+ {
+ id: "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ message: "Add simple search to projects in public area",
+ timestamp: "2013-05-13T18:18:08+00:00",
+ url: "https://test.example.com/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
+ author: {
+ name: "Test User",
+ email: "test@example.com"
+ }
+ }
+ ],
+ total_commits_count: 1
+ }.freeze
+
# Produce a hash of post-receive data
#
# data = {
@@ -74,6 +103,10 @@ module Gitlab
build(project, user, commits.last&.id, commits.first&.id, ref, commits)
end
+ def sample_data
+ SAMPLE_DATA
+ end
+
private
def checkout_sha(repository, newrev, ref)
diff --git a/lib/gitlab/data_builder/repository.rb b/lib/gitlab/data_builder/repository.rb
index b42dc052949..c9c13ec6487 100644
--- a/lib/gitlab/data_builder/repository.rb
+++ b/lib/gitlab/data_builder/repository.rb
@@ -3,6 +3,23 @@ module Gitlab
module Repository
extend self
+ SAMPLE_DATA = {
+ event_name: 'repository_update',
+ user_id: 10,
+ user_name: 'john.doe',
+ user_email: 'test@example.com',
+ user_avatar: 'http://example.com/avatar/user.png',
+ project_id: 40,
+ changes: [
+ {
+ before: "8205ea8d81ce0c6b90fbe8280d118cc9fdad6130",
+ after: "4045ea7a3df38697b3730a20fb73c8bed8a3e69e",
+ ref: "refs/heads/master"
+ }
+ ],
+ "refs": ["refs/heads/master"]
+ }.freeze
+
# Produce a hash of post-receive data
def update(project, user, changes, refs)
{
@@ -30,6 +47,10 @@ module Gitlab
ref: ref
}
end
+
+ def sample_data
+ SAMPLE_DATA
+ end
end
end
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index d7dab584a44..a6ec75da385 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -9,6 +9,14 @@ module Gitlab
ActiveRecord::Base.configurations[Rails.env]
end
+ def self.username
+ config['username'] || ENV['USER']
+ end
+
+ def self.database_name
+ config['database']
+ end
+
def self.adapter_name
config['adapter']
end
@@ -25,6 +33,10 @@ module Gitlab
database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
+ def self.join_lateral_supported?
+ postgresql? && version.to_f >= 9.3
+ end
+
def self.nulls_last_order(field, direction = 'ASC')
order = "#{field} #{direction}"
diff --git a/lib/gitlab/database/grant.rb b/lib/gitlab/database/grant.rb
new file mode 100644
index 00000000000..aee3981e79a
--- /dev/null
+++ b/lib/gitlab/database/grant.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ module Database
+ # Model that can be used for querying permissions of a SQL user.
+ class Grant < ActiveRecord::Base
+ self.table_name =
+ if Database.postgresql?
+ 'information_schema.role_table_grants'
+ else
+ 'mysql.user'
+ end
+
+ def self.scope_to_current_user
+ if Database.postgresql?
+ where('grantee = user')
+ else
+ where("CONCAT(User, '@', Host) = current_user()")
+ end
+ end
+
+ # Returns true if the current user can create and execute triggers on the
+ # given table.
+ def self.create_and_execute_trigger?(table)
+ priv =
+ if Database.postgresql?
+ where(privilege_type: 'TRIGGER', table_name: table)
+ else
+ where(Trigger_priv: 'Y')
+ end
+
+ priv.scope_to_current_user.any?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 69ca9aa596b..fb14798efe6 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -358,6 +358,8 @@ module Gitlab
raise 'rename_column_concurrently can not be run inside a transaction'
end
+ check_trigger_permissions!(table)
+
old_col = column_for(table, old)
new_type = type || old_col.type
@@ -430,6 +432,8 @@ module Gitlab
def cleanup_concurrent_column_rename(table, old, new)
trigger_name = rename_trigger_name(table, old, new)
+ check_trigger_permissions!(table)
+
if Database.postgresql?
remove_rename_triggers_for_postgresql(table, trigger_name)
else
@@ -485,14 +489,14 @@ module Gitlab
# Removes the triggers used for renaming a PostgreSQL column concurrently.
def remove_rename_triggers_for_postgresql(table, trigger)
- execute("DROP TRIGGER #{trigger} ON #{table}")
- execute("DROP FUNCTION #{trigger}()")
+ execute("DROP TRIGGER IF EXISTS #{trigger} ON #{table}")
+ execute("DROP FUNCTION IF EXISTS #{trigger}()")
end
# Removes the triggers used for renaming a MySQL column concurrently.
def remove_rename_triggers_for_mysql(trigger)
- execute("DROP TRIGGER #{trigger}_insert")
- execute("DROP TRIGGER #{trigger}_update")
+ execute("DROP TRIGGER IF EXISTS #{trigger}_insert")
+ execute("DROP TRIGGER IF EXISTS #{trigger}_update")
end
# Returns the (base) name to use for triggers when renaming columns.
@@ -606,6 +610,49 @@ module Gitlab
Arel::Nodes::SqlLiteral.new(replace.to_sql)
end
end
+
+ def remove_foreign_key_without_error(*args)
+ remove_foreign_key(*args)
+ rescue ArgumentError
+ end
+
+ def sidekiq_queue_migrate(queue_from, to:)
+ while sidekiq_queue_length(queue_from) > 0
+ Sidekiq.redis do |conn|
+ conn.rpoplpush "queue:#{queue_from}", "queue:#{to}"
+ end
+ end
+ end
+
+ def sidekiq_queue_length(queue_name)
+ Sidekiq.redis do |conn|
+ conn.llen("queue:#{queue_name}")
+ end
+ end
+
+ def check_trigger_permissions!(table)
+ unless Grant.create_and_execute_trigger?(table)
+ dbname = Database.database_name
+ user = Database.username
+
+ raise <<-EOF
+Your database user is not allowed to create, drop, or execute triggers on the
+table #{table}.
+
+If you are using PostgreSQL you can solve this by logging in to the GitLab
+database (#{dbname}) using a super user and running:
+
+ ALTER #{user} WITH SUPERUSER
+
+For MySQL you instead need to run:
+
+ GRANT ALL PRIVILEGES ON *.* TO #{user}@'%'
+
+Both queries will grant the user super user permissions, ensuring you don't run
+into similar problems in the future (e.g. when new tables are created).
+ EOF
+ end
+ end
end
end
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 6d7de52cb80..1dabd4ebdd0 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -186,7 +186,10 @@ module Gitlab
end
def content_changed?
- old_blob && new_blob && old_blob.id != new_blob.id
+ return blobs_changed? if diff_refs
+ return false if new_file? || deleted_file? || renamed_file?
+
+ text? && diff_lines.any?
end
def different_type?
@@ -225,6 +228,10 @@ module Gitlab
private
+ def blobs_changed?
+ old_blob && new_blob && old_blob.id != new_blob.id
+ end
+
def simple_viewer_class
return DiffViewer::NotDiffable unless diffable?
@@ -250,6 +257,8 @@ module Gitlab
DiffViewer::Renamed
elsif mode_changed?
DiffViewer::ModeChanged
+ else
+ DiffViewer::NoPreview
end
end
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 2d89ccfc354..0603141e441 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -21,7 +21,7 @@ module Gitlab
def to_hash
hash = {}
- serialize_keys.each { |key| hash[key] = send(key) }
+ serialize_keys.each { |key| hash[key] = send(key) } # rubocop:disable GitlabSecurity/PublicSend
hash
end
diff --git a/lib/gitlab/diff/line_mapper.rb b/lib/gitlab/diff/line_mapper.rb
index 576a761423e..cf71d47df8e 100644
--- a/lib/gitlab/diff/line_mapper.rb
+++ b/lib/gitlab/diff/line_mapper.rb
@@ -38,7 +38,7 @@ module Gitlab
# - The first diff line with a higher line number, if it falls between diff contexts
# - The last known diff line, if it falls after the last diff context
diff_line = diff_lines.find do |diff_line|
- diff_from_line = diff_line.send(from)
+ diff_from_line = diff_line.public_send(from) # rubocop:disable GitlabSecurity/PublicSend
diff_from_line && diff_from_line >= from_line
end
diff_line ||= diff_lines.last
@@ -47,8 +47,8 @@ module Gitlab
# mapped line number is the same as the specified line number.
return from_line unless diff_line
- diff_from_line = diff_line.send(from)
- diff_to_line = diff_line.send(to)
+ diff_from_line = diff_line.public_send(from) # rubocop:disable GitlabSecurity/PublicSend
+ diff_to_line = diff_line.public_send(to) # rubocop:disable GitlabSecurity/PublicSend
# If the line was removed, there is no mapped line number.
return unless diff_to_line
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index 72d7d4f84d1..abd401224d8 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -98,10 +98,11 @@ module Gitlab
if status.zero?
@ee_branch_found = ee_branch_prefix
- else
- _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch origin #{ee_branch_suffix}])
+ return
end
+ _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch origin #{ee_branch_suffix}])
+
if status.zero?
@ee_branch_found = ee_branch_suffix
else
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
index 781f9c56a42..7b3483a7f96 100644
--- a/lib/gitlab/encoding_helper.rb
+++ b/lib/gitlab/encoding_helper.rb
@@ -11,7 +11,7 @@ module Gitlab
# obscure encoding with low confidence.
# There is a lot more info with this merge request:
# https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193
- ENCODING_CONFIDENCE_THRESHOLD = 40
+ ENCODING_CONFIDENCE_THRESHOLD = 50
def encode!(message)
return nil unless message.respond_to? :force_encoding
@@ -22,10 +22,10 @@ module Gitlab
# return message if message type is binary
detect = CharlockHolmes::EncodingDetector.detect(message)
- return message.force_encoding("BINARY") if detect && detect[:type] == :binary
+ return message.force_encoding("BINARY") if detect_binary?(message, detect)
- # force detected encoding if we have sufficient confidence.
if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
+ # force detected encoding if we have sufficient confidence.
message.force_encoding(detect[:encoding])
end
@@ -36,6 +36,19 @@ module Gitlab
"--broken encoding: #{encoding}"
end
+ def detect_binary?(data, detect = nil)
+ detect ||= CharlockHolmes::EncodingDetector.detect(data)
+ detect && detect[:type] == :binary && detect[:confidence] == 100
+ end
+
+ def detect_libgit2_binary?(data)
+ # EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks
+ # only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15),
+ # which is what we use below to keep a consistent behavior.
+ detect = CharlockHolmes::EncodingDetector.new(8000).detect(data)
+ detect && detect[:type] == :binary
+ end
+
def encode_utf8(message)
detect = CharlockHolmes::EncodingDetector.detect(message)
if detect && detect[:encoding]
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index 3784f6c4947..3f7b42456af 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -25,6 +25,12 @@ module Gitlab
end
EOS
+ def self.get_uuid(key)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(redis_shared_state_key(key)) || false
+ end
+ end
+
def self.cancel(key, uuid)
Gitlab::Redis::SharedState.with do |redis|
redis.eval(LUA_CANCEL_SCRIPT, keys: [redis_shared_state_key(key)], argv: [uuid])
@@ -35,10 +41,10 @@ module Gitlab
"gitlab:exclusive_lease:#{key}"
end
- def initialize(key, timeout:)
+ def initialize(key, uuid: nil, timeout:)
@redis_shared_state_key = self.class.redis_shared_state_key(key)
@timeout = timeout
- @uuid = SecureRandom.uuid
+ @uuid = uuid || SecureRandom.uuid
end
# Try to obtain the lease. Return lease UUID on success,
diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb
index 093d9ed8092..10ffc345bd5 100644
--- a/lib/gitlab/file_finder.rb
+++ b/lib/gitlab/file_finder.rb
@@ -6,27 +6,48 @@ module Gitlab
attr_reader :project, :ref
+ delegate :repository, to: :project
+
def initialize(project, ref)
@project = project
@ref = ref
end
def find(query)
- blobs = project.repository.search_files_by_content(query, ref).first(BATCH_SIZE)
- found_file_names = Set.new
+ by_content = find_by_content(query)
- results = blobs.map do |blob|
- blob = Gitlab::ProjectSearchResults.parse_search_result(blob)
- found_file_names << blob.filename
+ already_found = Set.new(by_content.map(&:filename))
+ by_filename = find_by_filename(query, except: already_found)
- [blob.filename, blob]
- end
+ (by_content + by_filename)
+ .sort_by(&:filename)
+ .map { |blob| [blob.filename, blob] }
+ end
- project.repository.search_files_by_name(query, ref).first(BATCH_SIZE).each do |filename|
- results << [filename, OpenStruct.new(ref: ref)] unless found_file_names.include?(filename)
- end
+ private
- results.sort_by(&:first)
+ def find_by_content(query)
+ results = repository.search_files_by_content(query, ref).first(BATCH_SIZE)
+ results.map { |result| Gitlab::ProjectSearchResults.parse_search_result(result) }
+ end
+
+ def find_by_filename(query, except: [])
+ filenames = repository.search_files_by_name(query, ref).first(BATCH_SIZE)
+ filenames.delete_if { |filename| except.include?(filename) } unless except.empty?
+
+ blob_refs = filenames.map { |filename| [ref, filename] }
+ blobs = Gitlab::Git::Blob.batch(repository, blob_refs, blob_size_limit: 1024)
+
+ blobs.map do |blob|
+ Gitlab::SearchResults::FoundBlob.new(
+ id: blob.id,
+ filename: blob.path,
+ basename: File.basename(blob.path),
+ ref: ref,
+ startline: 1,
+ data: blob.data
+ )
+ end
end
end
end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index b6449f27034..8c9acbc9fbe 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -5,6 +5,7 @@ module Gitlab
BRANCH_REF_PREFIX = "refs/heads/".freeze
CommandError = Class.new(StandardError)
+ CommitError = Class.new(StandardError)
class << self
include Gitlab::EncodingHelper
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
index 8dbe25e55f6..31effdba292 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -16,7 +16,7 @@ module Gitlab
def each
@blames.each do |blame|
yield(
- Gitlab::Git::Commit.new(blame.commit),
+ Gitlab::Git::Commit.new(@repo, blame.commit),
blame.line
)
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 77b81d2d437..8d96826f6ee 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -42,19 +42,11 @@ module Gitlab
end
end
- def binary?(data)
- # EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks
- # only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15),
- # which is what we use below to keep a consistent behavior.
- detect = CharlockHolmes::EncodingDetector.new(8000).detect(data)
- detect && detect[:type] == :binary
- end
-
# Returns an array of Blob instances, specified in blob_references as
# [[commit_sha, path], [commit_sha, path], ...]. If blob_size_limit < 0 then the
# full blob contents are returned. If blob_size_limit >= 0 then each blob will
# contain no more than limit bytes in its data attribute.
- #
+ #
# Keep in mind that this method may allocate a lot of memory. It is up
# to the caller to limit the number of blobs and blob_size_limit.
#
@@ -65,6 +57,10 @@ module Gitlab
end
end
+ def binary?(data)
+ EncodingHelper.detect_libgit2_binary?(data)
+ end
+
private
# Recursive search of blob id by path
@@ -173,7 +169,7 @@ module Gitlab
def initialize(options)
%w(id name path size data mode commit_id binary).each do |key|
- self.send("#{key}=", options[key.to_sym])
+ self.__send__("#{key}=", options[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend
end
@loaded_all_data = false
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 600d886e818..5ee6669050c 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -14,7 +14,7 @@ module Gitlab
attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
- delegate :tree, to: :raw_commit
+ delegate :tree, to: :rugged_commit
def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit)
@@ -50,19 +50,29 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/321
def find(repo, commit_id = "HEAD")
+ # Already a commit?
return commit_id if commit_id.is_a?(Gitlab::Git::Commit)
- return decorate(commit_id) if commit_id.is_a?(Rugged::Commit)
- obj = if commit_id.is_a?(String)
- repo.rev_parse_target(commit_id)
- else
- Gitlab::Git::Ref.dereference_object(commit_id)
- end
+ # A rugged reference?
+ commit_id = Gitlab::Git::Ref.dereference_object(commit_id)
+ return decorate(repo, commit_id) if commit_id.is_a?(Rugged::Commit)
- return nil unless obj.is_a?(Rugged::Commit)
+ # Some weird thing?
+ return nil unless commit_id.is_a?(String)
- decorate(obj)
- rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, Gitlab::Git::Repository::NoRepository
+ commit = repo.gitaly_migrate(:find_commit) do |is_enabled|
+ if is_enabled
+ repo.gitaly_commit_client.find_commit(commit_id)
+ else
+ obj = repo.rev_parse_target(commit_id)
+
+ obj.is_a?(Rugged::Commit) ? obj : nil
+ end
+ end
+
+ decorate(repo, commit) if commit
+ rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError,
+ Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository
nil
end
@@ -102,7 +112,7 @@ module Gitlab
if is_enabled
repo.gitaly_commit_client.between(base, head)
else
- repo.rugged_commits_between(base, head).map { |c| decorate(c) }
+ repo.rugged_commits_between(base, head).map { |c| decorate(repo, c) }
end
end
rescue Rugged::ReferenceError
@@ -169,7 +179,7 @@ module Gitlab
offset = actual_options[:skip]
limit = actual_options[:max_count]
walker.each(offset: offset, limit: limit) do |commit|
- commits.push(decorate(commit))
+ commits.push(decorate(repo, commit))
end
walker.reset
@@ -183,27 +193,8 @@ module Gitlab
Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options)
end
- def decorate(commit, ref = nil)
- Gitlab::Git::Commit.new(commit, ref)
- end
-
- # Returns a diff object for the changes introduced by +rugged_commit+.
- # If +rugged_commit+ doesn't have a parent, then the diff is between
- # this commit and an empty repo. See Repository#diff for the keys
- # allowed in the +options+ hash.
- def diff_from_parent(rugged_commit, options = {})
- options ||= {}
- break_rewrites = options[:break_rewrites]
- actual_options = Gitlab::Git::Diff.filter_diff_options(options)
-
- diff = if rugged_commit.parents.empty?
- rugged_commit.diff(actual_options.merge(reverse: true))
- else
- rugged_commit.parents[0].diff(rugged_commit, actual_options)
- end
-
- diff.find_similar!(break_rewrites: break_rewrites)
- diff
+ def decorate(repository, commit, ref = nil)
+ Gitlab::Git::Commit.new(repository, commit, ref)
end
# Returns the `Rugged` sorting type constant for one or more given
@@ -219,9 +210,19 @@ module Gitlab
@rugged_sort_types.fetch(sort_type, Rugged::SORT_NONE)
end
+
+ def shas_with_signatures(repository, shas)
+ shas.select do |sha|
+ begin
+ Rugged::Commit.extract_signature(repository.rugged, sha)
+ rescue Rugged::OdbError
+ false
+ end
+ end
+ end
end
- def initialize(raw_commit, head = nil)
+ def initialize(repository, raw_commit, head = nil)
raise "Nil as raw commit passed" unless raw_commit
case raw_commit
@@ -229,12 +230,13 @@ module Gitlab
init_from_hash(raw_commit)
when Rugged::Commit
init_from_rugged(raw_commit)
- when Gitlab::GitalyClient::Commit
+ when Gitaly::GitCommit
init_from_gitaly(raw_commit)
else
raise "Invalid raw commit type: #{raw_commit.class}"
end
+ @repository = repository
@head = head
end
@@ -269,19 +271,56 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324
def to_diff
- diff_from_parent.patch
+ Gitlab::GitalyClient.migrate(:commit_patch) do |is_enabled|
+ if is_enabled
+ @repository.gitaly_commit_client.patch(id)
+ else
+ rugged_diff_from_parent.patch
+ end
+ end
end
# Returns a diff object for the changes from this commit's first parent.
# If there is no parent, then the diff is between this commit and an
- # empty repo. See Repository#diff for keys allowed in the +options+
+ # empty repo. See Repository#diff for keys allowed in the +options+
# hash.
def diff_from_parent(options = {})
- Commit.diff_from_parent(raw_commit, options)
+ Gitlab::GitalyClient.migrate(:commit_raw_diffs) do |is_enabled|
+ if is_enabled
+ @repository.gitaly_commit_client.diff_from_parent(self, options)
+ else
+ rugged_diff_from_parent(options)
+ end
+ end
+ end
+
+ def rugged_diff_from_parent(options = {})
+ options ||= {}
+ break_rewrites = options[:break_rewrites]
+ actual_options = Gitlab::Git::Diff.filter_diff_options(options)
+
+ diff = if rugged_commit.parents.empty?
+ rugged_commit.diff(actual_options.merge(reverse: true))
+ else
+ rugged_commit.parents[0].diff(rugged_commit, actual_options)
+ end
+
+ diff.find_similar!(break_rewrites: break_rewrites)
+ diff
end
def deltas
- @deltas ||= diff_from_parent.each_delta.map { |d| Gitlab::Git::Diff.new(d) }
+ @deltas ||= begin
+ deltas = Gitlab::GitalyClient.migrate(:commit_deltas) do |is_enabled|
+ if is_enabled
+ @repository.gitaly_commit_client.commit_deltas(self)
+ else
+ rugged_diff_from_parent.each_delta
+ end
+ end
+
+ deltas.map { |delta| Gitlab::Git::Diff.new(delta) }
+ end
end
def has_zero_stats?
@@ -296,7 +335,7 @@ module Gitlab
def to_hash
serialize_keys.map.with_object({}) do |key, hash|
- hash[key] = send(key)
+ hash[key] = send(key) # rubocop:disable GitlabSecurity/PublicSend
end
end
@@ -309,23 +348,7 @@ module Gitlab
end
def parents
- case raw_commit
- when Rugged::Commit
- raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) }
- when Gitlab::GitalyClient::Commit
- parent_ids.map { |oid| self.class.find(raw_commit.repository, oid) }.compact
- else
- raise NotImplementedError, "commit source doesn't support #parents"
- end
- end
-
- # Get the gpg signature of this commit.
- #
- # Ex.
- # commit.signature(repo)
- #
- def signature(repo)
- Rugged::Commit.extract_signature(repo.rugged, sha)
+ parent_ids.map { |oid| self.class.find(@repository, oid) }.compact
end
def stats
@@ -334,7 +357,7 @@ module Gitlab
def to_patch(options = {})
begin
- raw_commit.to_mbox(options)
+ rugged_commit.to_mbox(options)
rescue Rugged::InvalidError => ex
if ex.message =~ /commit \w+ is a merge commit/i
'Patch format is not currently supported for merge commits.'
@@ -382,13 +405,21 @@ module Gitlab
encode! @committer_email
end
+ def rugged_commit
+ @rugged_commit ||= if raw_commit.is_a?(Rugged::Commit)
+ raw_commit
+ else
+ @repository.rev_parse_target(id)
+ end
+ end
+
private
def init_from_hash(hash)
raw_commit = hash.symbolize_keys
serialize_keys.each do |key|
- send("#{key}=", raw_commit[key])
+ send("#{key}=", raw_commit[key]) # rubocop:disable GitlabSecurity/PublicSend
end
end
@@ -415,10 +446,10 @@ module Gitlab
# subject from the message to make it clearer when there's one
# available but not the other.
@message = (commit.body.presence || commit.subject).dup
- @authored_date = Time.at(commit.author.date.seconds)
+ @authored_date = Time.at(commit.author.date.seconds).utc
@author_name = commit.author.name.dup
@author_email = commit.author.email.dup
- @committed_date = Time.at(commit.committer.date.seconds)
+ @committed_date = Time.at(commit.committer.date.seconds).utc
@committer_name = commit.committer.name.dup
@committer_email = commit.committer.email.dup
@parent_ids = commit.parent_ids
diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb
index 57c29ad112c..00acb4763e9 100644
--- a/lib/gitlab/git/commit_stats.rb
+++ b/lib/gitlab/git/commit_stats.rb
@@ -16,7 +16,7 @@ module Gitlab
@deletions = 0
@total = 0
- diff = commit.diff_from_parent
+ diff = commit.rugged_diff_from_parent
diff.each_patch do |p|
# TODO: Use the new Rugged convenience methods when they're released
diff --git a/lib/gitlab/git/committer.rb b/lib/gitlab/git/committer.rb
new file mode 100644
index 00000000000..1f4bcf7a3a0
--- /dev/null
+++ b/lib/gitlab/git/committer.rb
@@ -0,0 +1,21 @@
+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 9e00abefd02..a23c8cf0dd1 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -116,6 +116,15 @@ module Gitlab
filtered_opts
end
+
+ # Return a binary diff message like:
+ #
+ # "Binary files a/file/path and b/file/path differ\n"
+ # This is used when we detect that a diff is binary
+ # using CharlockHolmes when Rugged treats it as text.
+ def binary_message(old_path, new_path)
+ "Binary files #{old_path} and #{new_path} differ\n"
+ end
end
def initialize(raw_diff, expanded: true)
@@ -143,7 +152,7 @@ module Gitlab
hash = {}
SERIALIZE_KEYS.each do |key|
- hash[key] = send(key)
+ hash[key] = send(key) # rubocop:disable GitlabSecurity/PublicSend
end
hash
@@ -190,6 +199,13 @@ module Gitlab
@collapsed = true
end
+ def json_safe_diff
+ return @diff unless detect_binary?(@diff)
+
+ # the diff is binary, let's make a message for it
+ Diff.binary_message(@old_path, @new_path)
+ end
+
private
def init_from_rugged(rugged)
@@ -221,7 +237,7 @@ module Gitlab
raw_diff = hash.symbolize_keys
SERIALIZE_KEYS.each do |key|
- send(:"#{key}=", raw_diff[key.to_sym])
+ send(:"#{key}=", raw_diff[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 87ed9c3ea26..6a601561c2a 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -28,7 +28,6 @@ module Gitlab
@limits = self.class.collection_limits(options)
@enforce_limits = !!options.fetch(:limits, true)
@expanded = !!options.fetch(:expanded, true)
- @from_gitaly = options.fetch(:from_gitaly, false)
@line_count = 0
@byte_count = 0
@@ -44,7 +43,7 @@ module Gitlab
return if @iterator.nil?
Gitlab::GitalyClient.migrate(:commit_raw_diffs) do |is_enabled|
- if is_enabled && @from_gitaly
+ if is_enabled && @iterator.is_a?(Gitlab::GitalyClient::DiffStitcher)
each_gitaly_patch(&block)
else
each_rugged_patch(&block)
diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb
index 8f0c377ef4f..cc35d77c6e4 100644
--- a/lib/gitlab/git/hook.rb
+++ b/lib/gitlab/git/hook.rb
@@ -1,20 +1,23 @@
-# Gitaly note: JV: looks like this is only used by GitHooksService in
+# Gitaly note: JV: looks like this is only used by Gitlab::Git::HooksService in
# app/services. We shouldn't bother migrating this until we know how
-# GitHooksService will be migrated.
+# Gitlab::Git::HooksService will be migrated.
module Gitlab
module Git
class Hook
GL_PROTOCOL = 'web'.freeze
- attr_reader :name, :repo_path, :path
+ attr_reader :name, :path, :repository
- def initialize(name, project)
+ def initialize(name, repository)
@name = name
- @project = project
- @repo_path = project.repository.path
+ @repository = repository
@path = File.join(repo_path.strip, 'hooks', name)
end
+ def repo_path
+ repository.path
+ end
+
def exists?
File.exist?(path)
end
@@ -44,7 +47,7 @@ module Gitlab
'GL_ID' => gl_id,
'PWD' => repo_path,
'GL_PROTOCOL' => GL_PROTOCOL,
- 'GL_REPOSITORY' => Gitlab::GlRepository.gl_repository(@project, false)
+ 'GL_REPOSITORY' => repository.gl_repository
}
options = {
diff --git a/lib/gitlab/git/hooks_service.rb b/lib/gitlab/git/hooks_service.rb
new file mode 100644
index 00000000000..ea8a87a1290
--- /dev/null
+++ b/lib/gitlab/git/hooks_service.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Git
+ class HooksService
+ PreReceiveError = Class.new(StandardError)
+
+ 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
+
+ %w(pre-receive update).each do |hook_name|
+ status, message = run_hook(hook_name)
+
+ unless status
+ raise PreReceiveError, message
+ end
+ end
+
+ yield(self).tap do
+ run_hook('post-receive')
+ end
+ end
+
+ private
+
+ def run_hook(name)
+ hook = Gitlab::Git::Hook.new(name, @repository)
+ hook.trigger(@gl_id, oldrev, newrev, ref)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb
new file mode 100644
index 00000000000..9e6fca8c80c
--- /dev/null
+++ b/lib/gitlab/git/operation_service.rb
@@ -0,0 +1,168 @@
+module Gitlab
+ module Git
+ class OperationService
+ attr_reader :committer, :repository
+
+ def initialize(committer, new_repository)
+ committer = Gitlab::Git::Committer.from_user(committer) if committer.is_a?(User)
+ @committer = committer
+
+ # Refactoring aid
+ unless new_repository.is_a?(Gitlab::Git::Repository)
+ raise "expected a Gitlab::Git::Repository, got #{new_repository}"
+ end
+
+ @repository = new_repository
+ end
+
+ def add_branch(branch_name, newrev)
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
+ oldrev = Gitlab::Git::BLANK_SHA
+
+ update_ref_in_hooks(ref, newrev, oldrev)
+ end
+
+ def rm_branch(branch)
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name
+ oldrev = branch.target
+ newrev = Gitlab::Git::BLANK_SHA
+
+ update_ref_in_hooks(ref, newrev, oldrev)
+ end
+
+ def add_tag(tag_name, newrev, options = {})
+ ref = Gitlab::Git::TAG_REF_PREFIX + tag_name
+ oldrev = Gitlab::Git::BLANK_SHA
+
+ with_hooks(ref, newrev, oldrev) do |service|
+ # We want to pass the OID of the tag object to the hooks. For an
+ # annotated tag we don't know that OID until after the tag object
+ # (raw_tag) is created in the repository. That is why we have to
+ # update the value after creating the tag object. Only the
+ # "post-receive" hook will receive the correct value in this case.
+ raw_tag = repository.rugged.tags.create(tag_name, newrev, options)
+ service.newrev = raw_tag.target_id
+ end
+ end
+
+ def rm_tag(tag)
+ ref = Gitlab::Git::TAG_REF_PREFIX + tag.name
+ oldrev = tag.target
+ newrev = Gitlab::Git::BLANK_SHA
+
+ update_ref_in_hooks(ref, newrev, oldrev) do
+ repository.rugged.tags.delete(tag_name)
+ end
+ end
+
+ # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist,
+ # it would be created from `start_branch_name`.
+ # If `start_project` is passed, and the branch doesn't exist,
+ # it would try to find the commits from it instead of current repository.
+ def with_branch(
+ branch_name,
+ start_branch_name: nil,
+ start_repository: repository,
+ &block)
+
+ # Refactoring aid
+ unless start_repository.is_a?(Gitlab::Git::Repository)
+ raise "expected a Gitlab::Git::Repository, got #{start_repository}"
+ end
+
+ start_branch_name = nil if start_repository.empty_repo?
+
+ if start_branch_name && !start_repository.branch_exists?(start_branch_name)
+ raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.full_path}"
+ end
+
+ update_branch_with_hooks(branch_name) do
+ repository.with_repo_branch_commit(
+ start_repository,
+ start_branch_name || branch_name,
+ &block)
+ end
+ end
+
+ private
+
+ # Returns [newrev, should_run_after_create, should_run_after_create_branch]
+ def update_branch_with_hooks(branch_name)
+ update_autocrlf_option
+
+ was_empty = repository.empty?
+
+ # Make commit
+ newrev = yield
+
+ unless newrev
+ raise Gitlab::Git::CommitError.new('Failed to create commit')
+ end
+
+ branch = repository.find_branch(branch_name)
+ oldrev = find_oldrev_from_branch(newrev, branch)
+
+ 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)]
+ end
+
+ def find_oldrev_from_branch(newrev, branch)
+ return Gitlab::Git::BLANK_SHA unless branch
+
+ oldrev = branch.target
+
+ if oldrev == repository.rugged.merge_base(newrev, branch.target)
+ oldrev
+ else
+ raise Gitlab::Git::CommitError.new('Branch diverged')
+ end
+ end
+
+ def update_ref_in_hooks(ref, newrev, oldrev)
+ with_hooks(ref, newrev, oldrev) do
+ update_ref(ref, newrev, oldrev)
+ end
+ end
+
+ def with_hooks(ref, newrev, oldrev)
+ Gitlab::Git::HooksService.new.execute(
+ committer,
+ repository,
+ oldrev,
+ newrev,
+ ref) do |service|
+
+ yield(service)
+ end
+ end
+
+ # Gitaly note: JV: wait with migrating #update_ref until we know how to migrate its call sites.
+ def update_ref(ref, newrev, oldrev)
+ # We use 'git update-ref' because libgit2/rugged currently does not
+ # offer 'compare and swap' ref updates. Without compare-and-swap we can
+ # (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(
+ command,
+ repository.path) do |stdin|
+ stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00")
+ end
+
+ unless status.zero?
+ raise Gitlab::Git::CommitError.new(
+ "Could not update branch #{Gitlab::Git.branch_name(ref)}." \
+ " Please refresh and try again.")
+ end
+ end
+
+ def update_autocrlf_option
+ if repository.autocrlf != :input
+ repository.autocrlf = :input
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index f246393cfbc..efa13590a2c 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -17,6 +17,30 @@ module Gitlab
NoRepository = Class.new(StandardError)
InvalidBlobName = Class.new(StandardError)
InvalidRef = Class.new(StandardError)
+ GitError = Class.new(StandardError)
+ DeleteBranchError = 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')
+
+ FileUtils.mkdir_p(repo_path, mode: 0770)
+
+ # Equivalent to `git --git-path=#{repo_path} init [--bare]`
+ repo = Rugged::Repository.init_at(repo_path, bare)
+ repo.close
+
+ if symlink_hooks_to.present?
+ hooks_path = File.join(repo_path, 'hooks')
+ FileUtils.rm_rf(hooks_path)
+ FileUtils.ln_s(symlink_hooks_to, hooks_path)
+ end
+
+ true
+ end
+ end
# Full path to repo
attr_reader :path
@@ -24,16 +48,20 @@ module Gitlab
# Directory name of repo
attr_reader :name
+ # Relative path of repo
+ attr_reader :relative_path
+
# Rugged repo object
attr_reader :rugged
- attr_reader :storage
+ attr_reader :storage, :gl_repository, :relative_path
# 'path' must be the path to a _bare_ git repository, e.g.
# /path/to/my-repo.git
- def initialize(storage, relative_path)
+ def initialize(storage, relative_path, gl_repository)
@storage = storage
@relative_path = relative_path
+ @gl_repository = gl_repository
storage_path = Gitlab.config.repositories.storages[@storage]['path']
@path = File.join(storage_path, @relative_path)
@@ -42,11 +70,14 @@ module Gitlab
end
delegate :empty?,
- :bare?,
to: :rugged
delegate :exists?, to: :gitaly_repository_client
+ def ==(other)
+ path == other.path
+ end
+
# Default branch in the repository
def root_ref
@root_ref ||= gitaly_migrate(:root_ref) do |is_enabled|
@@ -105,12 +136,18 @@ module Gitlab
# be stale/invalid when packed-refs is changed.
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333
def find_branch(name, force_reload = false)
- reload_rugged if force_reload
+ gitaly_migrate(:find_branch) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.find_branch(name)
+ else
+ reload_rugged if force_reload
- rugged_ref = rugged.branches[name]
- if rugged_ref
- target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
- Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
+ rugged_ref = rugged.branches[name]
+ if rugged_ref
+ target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
+ end
+ end
end
end
@@ -130,7 +167,7 @@ module Gitlab
if is_enabled
gitaly_ref_client.count_branch_names
else
- rugged.branches.count do |ref|
+ rugged.branches.each(:local).count do |ref|
begin
ref.name && ref.target # ensures the branch is valid
@@ -178,25 +215,43 @@ module Gitlab
end
end
+ # Returns true if the given ref name exists
+ #
+ # Ref names must start with `refs/`.
+ def ref_exists?(ref_name)
+ gitaly_migrate(:ref_exists) do |is_enabled|
+ if is_enabled
+ gitaly_ref_exists?(ref_name)
+ else
+ rugged_ref_exists?(ref_name)
+ end
+ end
+ end
+
# Returns true if the given tag exists
#
# name - The name of the tag as a String.
def tag_exists?(name)
- !!rugged.tags[name]
+ gitaly_migrate(:ref_exists_tags) do |is_enabled|
+ if is_enabled
+ gitaly_ref_exists?("refs/tags/#{name}")
+ else
+ rugged_tag_exists?(name)
+ end
+ end
end
# Returns true if the given branch exists
#
# name - The name of the branch as a String.
def branch_exists?(name)
- rugged.branches.exists?(name)
-
- # If the branch name is invalid (e.g. ".foo") Rugged will raise an error.
- # Whatever code calls this method shouldn't have to deal with that so
- # instead we just return `false` (which is true since a branch doesn't
- # exist when it has an invalid name).
- rescue Rugged::ReferenceError
- false
+ gitaly_migrate(:ref_exists_branches) do |is_enabled|
+ if is_enabled
+ gitaly_ref_exists?("refs/heads/#{name}")
+ else
+ rugged_branch_exists?(name)
+ end
+ end
end
# Returns an Array of branch and tag names
@@ -204,8 +259,17 @@ module Gitlab
branch_names + tag_names
end
- def has_commits?
- !empty?
+ def delete_all_refs_except(prefixes)
+ delete_refs(*all_ref_names_except(prefixes))
+ end
+
+ # Returns an Array of all ref names, except when it's matching pattern
+ #
+ # regexp - The pattern for ref names we don't want
+ def all_ref_names_except(prefixes)
+ rugged.references.reject do |ref|
+ prefixes.any? { |p| ref.name.start_with?(p) }
+ end.map(&:name)
end
# Discovers the default branch based on the repository's available branches
@@ -321,7 +385,24 @@ module Gitlab
options[:limit] ||= 0
options[:offset] ||= 0
- raw_log(options).map { |c| Commit.decorate(c) }
+ raw_log(options).map { |c| Commit.decorate(self, c) }
+ end
+
+ # Used in gitaly-ruby
+ def raw_log(options)
+ actual_ref = options[:ref] || root_ref
+ begin
+ sha = sha_from_ref(actual_ref)
+ rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
+ # Return an empty array if the ref wasn't found
+ return []
+ end
+
+ if log_using_shell?(options)
+ log_by_shell(sha, options)
+ else
+ log_by_walk(sha, options)
+ end
end
def count_commits(options)
@@ -384,8 +465,8 @@ module Gitlab
end
# Returns true is +from+ is direct ancestor to +to+, otherwise false
- def is_ancestor?(from, to)
- gitaly_commit_client.is_ancestor(from, to)
+ def ancestor?(from, to)
+ gitaly_commit_client.ancestor?(from, to)
end
# Return an array of Diff objects that represent the diff
@@ -529,9 +610,77 @@ 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
+
+ OperationService.new(committer, self).add_branch(branch_name, target_object.oid)
+ find_branch(branch_name)
+ rescue Rugged::ReferenceError => ex
+ raise InvalidRef, ex
+ 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
+
+ committer = Committer.from_user(committer) if committer.is_a?(User)
+
+ options = nil # Use nil, not the empty hash. Rugged cares about this.
+ if message
+ options = {
+ message: message,
+ tagger: Gitlab::Git.committer_hash(email: committer.email, name: committer.name)
+ }
+ end
+
+ OperationService.new(committer, self).add_tag(tag_name, target_object.oid, options)
+
+ find_tag(tag_name)
+ rescue Rugged::ReferenceError => ex
+ raise InvalidRef, ex
+ end
+
+ def rm_branch(branch_name, committer:)
+ OperationService.new(committer, self).rm_branch(find_branch(branch_name))
+ end
+
+ def rm_tag(tag_name, committer:)
+ OperationService.new(committer, self).rm_tag(find_tag(tag_name))
+ end
+
+ def find_tag(name)
+ tags.find { |tag| tag.name == name }
+ end
+
# Delete the specified branch from the repository
def delete_branch(branch_name)
- rugged.branches.delete(branch_name)
+ gitaly_migrate(:delete_branch) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.delete_branch(branch_name)
+ else
+ rugged.branches.delete(branch_name)
+ end
+ end
+ rescue Rugged::ReferenceError, CommandError => e
+ raise DeleteBranchError, e
+ end
+
+ def delete_refs(*ref_names)
+ instructions = ref_names.map do |ref|
+ "delete #{ref}\x00\x00"
+ end
+
+ command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
+ message, status = Gitlab::Popen.popen(
+ command,
+ path) do |stdin|
+ stdin.write(instructions.join)
+ end
+
+ unless status.zero?
+ raise GitError.new("Could not delete refs #{ref_names}: #{message}")
+ end
end
# Create a new branch named **ref+ based on **stat_point+, HEAD by default
@@ -540,46 +689,35 @@ module Gitlab
# create_branch("feature")
# create_branch("other-feature", "master")
def create_branch(ref, start_point = "HEAD")
- rugged_ref = rugged.branches.create(ref, start_point)
- 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 => e
- raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/
- raise InvalidRef.new("Invalid reference #{start_point}")
- end
-
- # Return an array of this repository's remote names
- def remote_names
- rugged.remotes.each_name.to_a
+ gitaly_migrate(:create_branch) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.create_branch(ref, start_point)
+ else
+ rugged_create_branch(ref, start_point)
+ end
+ end
end
# Delete the specified remote from this repository.
def remote_delete(remote_name)
rugged.remotes.delete(remote_name)
+ nil
end
- # Add a new remote to this repository. Returns a Rugged::Remote object
+ # Add a new remote to this repository.
def remote_add(remote_name, url)
rugged.remotes.create(remote_name, url)
+ nil
end
# Update the specified remote using the values in the +options+ hash
#
# Example
# repo.update_remote("origin", url: "path/to/repo")
- def remote_update(remote_name, options = {})
+ def remote_update(remote_name, url:)
# TODO: Implement other remote options
- rugged.remotes.set_url(remote_name, options[:url]) if options[:url]
- end
-
- # Fetch the specified remote
- def fetch(remote_name)
- rugged.remotes[remote_name].fetch
- end
-
- # Push +*refspecs+ to the remote identified by +remote_name+.
- def push(remote_name, *refspecs)
- rugged.remotes[remote_name].push(refspecs)
+ rugged.remotes.set_url(remote_name, url)
+ nil
end
AUTOCRLF_VALUES = {
@@ -603,60 +741,26 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/327
def ls_files(ref)
- actual_ref = ref || root_ref
-
- begin
- sha_from_ref(actual_ref)
- rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
- # Return an empty array if the ref wasn't found
- return []
- end
-
- cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree)
- cmd += %w(-r)
- cmd += %w(--full-tree)
- cmd += %w(--full-name)
- cmd += %W(-- #{actual_ref})
-
- raw_output = IO.popen(cmd, &:read).split("\n").map do |f|
- stuff, path = f.split("\t")
- _mode, type, _sha = stuff.split(" ")
- path if type == "blob"
- # Contain only blob type
+ gitaly_migrate(:ls_files) do |is_enabled|
+ if is_enabled
+ gitaly_ls_files(ref)
+ else
+ git_ls_files(ref)
+ end
end
-
- raw_output.compact
end
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/328
def copy_gitattributes(ref)
- begin
- commit = lookup(ref)
- rescue Rugged::ReferenceError
- raise InvalidRef.new("Ref #{ref} is invalid")
- end
-
- # Create the paths
- info_dir_path = File.join(path, 'info')
- info_attributes_path = File.join(info_dir_path, 'attributes')
-
- begin
- # Retrieve the contents of the blob
- gitattributes_content = blob_content(commit, '.gitattributes')
- rescue InvalidBlobName
- # No .gitattributes found. Should now remove any info/attributes and return
- File.delete(info_attributes_path) if File.exist?(info_attributes_path)
- return
- end
-
- # Create the info directory if needed
- Dir.mkdir(info_dir_path) unless File.directory?(info_dir_path)
-
- # Write the contents of the .gitattributes file to info/attributes
- # Use binary mode to prevent Rails from converting ASCII-8BIT to UTF-8
- File.open(info_attributes_path, "wb") do |file|
- file.write(gitattributes_content)
+ Gitlab::GitalyClient.migrate(:apply_gitattributes) do |is_enabled|
+ if is_enabled
+ gitaly_copy_gitattributes(ref)
+ else
+ rugged_copy_gitattributes(ref)
+ end
end
+ rescue GRPC::InvalidArgument
+ raise InvalidRef
end
# Returns the Git attributes for the given file path.
@@ -693,6 +797,106 @@ module Gitlab
end
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)
+
+ 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
+
+ if branch_commit = commit(sha)
+ yield branch_commit
+ else
+ with_repo_tmp_commit(
+ start_repository, start_branch_name, sha) do |tmp_commit|
+ yield tmp_commit
+ end
+ end
+ end
+ end
+
+ 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"
+ )
+
+ yield commit(sha)
+ ensure
+ delete_refs(tmp_ref) if tmp_ref
+ end
+
+ def fetch_source_branch(source_repository, source_branch, local_ref)
+ with_repo_branch_commit(source_repository, source_branch) do |commit|
+ if commit
+ write_ref(local_ref, commit.sha)
+ else
+ raise Rugged::ReferenceError, 'source repository is empty'
+ end
+ end
+ end
+
+ def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
+ with_repo_branch_commit(source_repository, source_branch_name) do |commit|
+ break unless commit
+
+ Gitlab::Git::Compare.new(
+ self,
+ target_branch_name,
+ commit.sha,
+ straight: straight
+ )
+ end
+ end
+
+ def write_ref(ref_path, sha)
+ rugged.references.create(ref_path, sha, force: true)
+ 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)
+
+ # Make sure ref was created, and raise Rugged::ReferenceError when not
+ raise Rugged::ReferenceError, message if status != 0
+
+ target_ref
+ end
+
+ # Refactoring aid; allows us to copy code from app/models/repository.rb
+ def run_git(args)
+ circuit_breaker.perform do
+ popen([Gitlab.config.git.bin_path, *args], path)
+ end
+ end
+
+ # Refactoring aid; allows us to copy code from app/models/repository.rb
+ def commit(ref = 'HEAD')
+ Gitlab::Git::Commit.find(self, ref)
+ end
+
+ # Refactoring aid; allows us to copy code from app/models/repository.rb
+ def empty_repo?
+ !exists? || !has_visible_content?
+ end
+
+ #
+ # Git repository can contains some hidden refs like:
+ # /refs/notes/*
+ # /refs/git-as-svn/*
+ # /refs/pulls/*
+ # This refs by default not visible in project page and not cloned to client side.
+ #
+ # This method return true if repository contains some content visible in project page.
+ #
+ def has_visible_content?
+ branch_count > 0
+ end
+
def gitaly_repository
Gitlab::GitalyClient::Util.repository(@storage, @relative_path)
end
@@ -733,22 +937,6 @@ module Gitlab
sort_branches(branches, sort_by)
end
- def raw_log(options)
- actual_ref = options[:ref] || root_ref
- begin
- sha = sha_from_ref(actual_ref)
- rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
- # Return an empty array if the ref wasn't found
- return []
- end
-
- if log_using_shell?(options)
- log_by_shell(sha, options)
- else
- log_by_walk(sha, options)
- end
- end
-
def log_using_shell?(options)
options[:path].present? ||
options[:disable_walk] ||
@@ -826,6 +1014,8 @@ module Gitlab
return unless commit_object && commit_object.type == :COMMIT
gitmodules = gitaly_commit_client.tree_entry(ref, '.gitmodules', Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
+ return unless gitmodules
+
found_module = GitmodulesParser.new(gitmodules.data).parse[path]
found_module && found_module['url']
@@ -973,6 +1163,117 @@ module Gitlab
raw_output.to_i
end
+
+ def gitaly_ls_files(ref)
+ gitaly_commit_client.ls_files(ref)
+ end
+
+ def git_ls_files(ref)
+ actual_ref = ref || root_ref
+
+ begin
+ sha_from_ref(actual_ref)
+ rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
+ # Return an empty array if the ref wasn't found
+ return []
+ end
+
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-tree)
+ cmd += %w(-r)
+ cmd += %w(--full-tree)
+ cmd += %w(--full-name)
+ cmd += %W(-- #{actual_ref})
+
+ raw_output = IO.popen(cmd, &:read).split("\n").map do |f|
+ stuff, path = f.split("\t")
+ _mode, type, _sha = stuff.split(" ")
+ path if type == "blob"
+ # Contain only blob type
+ end
+
+ raw_output.compact
+ end
+
+ # Returns true if the given ref name exists
+ #
+ # Ref names must start with `refs/`.
+ def rugged_ref_exists?(ref_name)
+ raise ArgumentError, 'invalid refname' unless ref_name.start_with?('refs/')
+ rugged.references.exist?(ref_name)
+ rescue Rugged::ReferenceError
+ false
+ end
+
+ # Returns true if the given ref name exists
+ #
+ # Ref names must start with `refs/`.
+ def gitaly_ref_exists?(ref_name)
+ gitaly_ref_client.ref_exists?(ref_name)
+ end
+
+ # Returns true if the given tag exists
+ #
+ # name - The name of the tag as a String.
+ def rugged_tag_exists?(name)
+ !!rugged.tags[name]
+ end
+
+ # Returns true if the given branch exists
+ #
+ # name - The name of the branch as a String.
+ def rugged_branch_exists?(name)
+ rugged.branches.exists?(name)
+
+ # If the branch name is invalid (e.g. ".foo") Rugged will raise an error.
+ # Whatever code calls this method shouldn't have to deal with that so
+ # instead we just return `false` (which is true since a branch doesn't
+ # exist when it has an invalid name).
+ rescue Rugged::ReferenceError
+ false
+ 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)
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
+ rescue Rugged::ReferenceError => e
+ raise InvalidRef.new("Branch #{ref} already exists") if e.to_s =~ /'refs\/heads\/#{ref}'/
+ raise InvalidRef.new("Invalid reference #{start_point}")
+ end
+
+ def gitaly_copy_gitattributes(revision)
+ gitaly_repository_client.apply_gitattributes(revision)
+ end
+
+ def rugged_copy_gitattributes(ref)
+ begin
+ commit = lookup(ref)
+ rescue Rugged::ReferenceError
+ raise InvalidRef.new("Ref #{ref} is invalid")
+ end
+
+ # Create the paths
+ info_dir_path = File.join(path, 'info')
+ info_attributes_path = File.join(info_dir_path, 'attributes')
+
+ begin
+ # Retrieve the contents of the blob
+ gitattributes_content = blob_content(commit, '.gitattributes')
+ rescue InvalidBlobName
+ # No .gitattributes found. Should now remove any info/attributes and return
+ File.delete(info_attributes_path) if File.exist?(info_attributes_path)
+ return
+ end
+
+ # Create the info directory if needed
+ Dir.mkdir(info_dir_path) unless File.directory?(info_dir_path)
+
+ # Write the contents of the .gitattributes file to info/attributes
+ # Use binary mode to prevent Rails from converting ASCII-8BIT to UTF-8
+ File.open(info_attributes_path, "wb") do |file|
+ file.write(gitattributes_content)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index 8e959c57c7c..5cf336af3c6 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -5,7 +5,7 @@ module Gitlab
class Tree
include Gitlab::EncodingHelper
- attr_accessor :id, :root_id, :name, :path, :type,
+ attr_accessor :id, :root_id, :name, :path, :flat_path, :type,
:mode, :commit_id, :submodule_url
class << self
@@ -19,8 +19,7 @@ module Gitlab
Gitlab::GitalyClient.migrate(:tree_entries) do |is_enabled|
if is_enabled
- client = Gitlab::GitalyClient::CommitService.new(repository)
- client.tree_entries(repository, sha, path)
+ repository.gitaly_commit_client.tree_entries(repository, sha, path)
else
tree_entries_from_rugged(repository, sha, path)
end
@@ -88,8 +87,8 @@ module Gitlab
end
def initialize(options)
- %w(id root_id name path type mode commit_id).each do |key|
- self.send("#{key}=", options[key.to_sym])
+ %w(id root_id name path flat_path type mode commit_id).each do |key|
+ self.send("#{key}=", options[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend
end
end
@@ -101,6 +100,10 @@ module Gitlab
encode! @path
end
+ def flat_path
+ encode! @flat_path
+ end
+
def dir?
type == :tree
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 0b62911958d..62d1ecae676 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -4,6 +4,7 @@ module Gitlab
class GitAccess
UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError)
+ ProjectMovedError = Class.new(NotFoundError)
ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for this project.',
@@ -34,6 +35,7 @@ module Gitlab
def check(cmd, changes)
check_protocol!
+ check_valid_actor!
check_active_user!
check_project_accessibility!
check_project_moved!
@@ -69,6 +71,14 @@ module Gitlab
private
+ def check_valid_actor!
+ return unless actor.is_a?(Key)
+
+ unless actor.valid?
+ raise UnauthorizedError, "Your SSH key #{actor.errors[:key].first}."
+ end
+ end
+
def check_protocol!
unless protocol_allowed?
raise UnauthorizedError, "Git access over #{protocol.upcase} is not allowed"
@@ -90,18 +100,18 @@ module Gitlab
end
def check_project_moved!
- if redirected_path
- url = protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo
- message = <<-MESSAGE.strip_heredoc
- Project '#{redirected_path}' was moved to '#{project.full_path}'.
+ return unless redirected_path
- Please update your Git remote and try again:
+ url = protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo
+ message = <<-MESSAGE.strip_heredoc
+ Project '#{redirected_path}' was moved to '#{project.full_path}'.
- git remote set-url origin #{url}
- MESSAGE
+ Please update your Git remote and try again:
- raise NotFoundError, message
- end
+ git remote set-url origin #{url}
+ MESSAGE
+
+ raise ProjectMovedError, message
end
def check_command_disabled!(cmd)
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index c90ef282fdd..a3dc2cd0b60 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -55,7 +55,7 @@ module Gitlab
def self.call(storage, service, rpc, request)
metadata = request_metadata(storage)
metadata = yield(metadata) if block_given?
- stub(service, storage).send(rpc, request, metadata)
+ stub(service, storage).__send__(rpc, request, metadata) # rubocop:disable GitlabSecurity/PublicSend
end
def self.request_metadata(storage)
@@ -70,21 +70,41 @@ module Gitlab
params['gitaly_token'].presence || Gitlab.config.gitaly['token']
end
- def self.feature_enabled?(feature, status: MigrationStatus::OPT_IN)
+ # Evaluates whether a feature toggle is on or off
+ def self.feature_enabled?(feature_name, status: MigrationStatus::OPT_IN)
+ # Disabled features are always off!
return false if status == MigrationStatus::DISABLED
- feature = Feature.get("gitaly_#{feature}")
+ feature = Feature.get("gitaly_#{feature_name}")
- # If the feature hasn't been set, turn it on if it's opt-out
- return status == MigrationStatus::OPT_OUT unless Feature.persisted?(feature)
+ # If the feature has been set, always evaluate
+ if Feature.persisted?(feature)
+ if feature.percentage_of_time_value > 0
+ # Probabilistically enable this feature
+ return Random.rand() * 100 < feature.percentage_of_time_value
+ end
+
+ return feature.enabled?
+ end
- if feature.percentage_of_time_value > 0
- # Probabilistically enable this feature
- return Random.rand() * 100 < feature.percentage_of_time_value
+ # If the feature has not been set, the default depends
+ # on it's status
+ case status
+ when MigrationStatus::OPT_OUT
+ true
+ when MigrationStatus::OPT_IN
+ opt_into_all_features?
+ else
+ false
end
+ end
- feature.enabled?
+ # opt_into_all_features? returns true when the current environment
+ # is one in which we opt into features automatically
+ def self.opt_into_all_features?
+ Rails.env.development? || ENV["GITALY_FEATURE_DEFAULT_ON"] == "1"
end
+ private_class_method :opt_into_all_features?
def self.migrate(feature, status: MigrationStatus::OPT_IN)
is_enabled = feature_enabled?(feature, status: status)
@@ -100,5 +120,9 @@ module Gitlab
path = Rails.root.join(SERVER_VERSION_FILE)
path.read.chomp
end
+
+ def self.encode(s)
+ s.dup.force_encoding(Encoding::ASCII_8BIT)
+ end
end
end
diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb
index 7ea8e8d0857..a250eb75bd4 100644
--- a/lib/gitlab/gitaly_client/blob_service.rb
+++ b/lib/gitlab/gitaly_client/blob_service.rb
@@ -13,10 +13,17 @@ module Gitlab
)
response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_blob, request)
- blob = response.first
- return unless blob.oid.present?
+ data = ''
+ blob = nil
+ response.each do |msg|
+ if blob.nil?
+ blob = msg
+ end
- data = response.reduce(blob.data.dup) { |memo, msg| memo << msg.data.dup }
+ data << msg.data
+ end
+
+ return nil if blob.oid.blank?
Gitlab::Git::Blob.new(
id: blob.oid,
diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb
deleted file mode 100644
index 61fe462d762..00000000000
--- a/lib/gitlab/gitaly_client/commit.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-module Gitlab
- module GitalyClient
- class Commit
- attr_reader :repository, :gitaly_commit
-
- delegate :id, :subject, :body, :author, :committer, :parent_ids, to: :gitaly_commit
-
- def initialize(repository, gitaly_commit)
- @repository = repository
- @gitaly_commit = gitaly_commit
- end
- end
- end
-end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 3f577ac8530..0825a3a7694 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -10,7 +10,19 @@ module Gitlab
@repository = repository
end
- def is_ancestor(ancestor_id, child_id)
+ def ls_files(revision)
+ request = Gitaly::ListFilesRequest.new(
+ repository: @gitaly_repo,
+ revision: GitalyClient.encode(revision)
+ )
+
+ response = GitalyClient.call(@repository.storage, :commit_service, :list_files, request)
+ response.flat_map do |msg|
+ msg.paths.map { |d| d.dup.force_encoding(Encoding::UTF_8) }
+ end
+ end
+
+ def ancestor?(ancestor_id, child_id)
request = Gitaly::CommitIsAncestorRequest.new(
repository: @gitaly_repo,
ancestor_id: ancestor_id,
@@ -29,56 +41,61 @@ module Gitlab
request = Gitaly::CommitDiffRequest.new(request_params)
response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request)
- Gitlab::Git::DiffCollection.new(GitalyClient::DiffStitcher.new(response), options.merge(from_gitaly: true))
+ GitalyClient::DiffStitcher.new(response)
end
def commit_deltas(commit)
request = Gitaly::CommitDeltaRequest.new(commit_diff_request_params(commit))
response = GitalyClient.call(@repository.storage, :diff_service, :commit_delta, request)
- response.flat_map do |msg|
- msg.deltas.map { |d| Gitlab::Git::Diff.new(d) }
- end
+
+ response.flat_map { |msg| msg.deltas }
end
def tree_entry(ref, path, limit = nil)
request = Gitaly::TreeEntryRequest.new(
repository: @gitaly_repo,
revision: ref,
- path: path.dup.force_encoding(Encoding::ASCII_8BIT),
+ path: GitalyClient.encode(path),
limit: limit.to_i
)
response = GitalyClient.call(@repository.storage, :commit_service, :tree_entry, request)
- entry = response.first
- return unless entry.oid.present?
- if entry.type == :BLOB
- rest_of_data = response.reduce("") { |memo, msg| memo << msg.data }
- entry.data += rest_of_data
+ entry = nil
+ data = ''
+ response.each do |msg|
+ if entry.nil?
+ entry = msg
+
+ break unless entry.type == :BLOB
+ end
+
+ data << msg.data
end
+ entry.data = data
- entry
+ entry unless entry.oid.blank?
end
def tree_entries(repository, revision, path)
request = Gitaly::GetTreeEntriesRequest.new(
repository: @gitaly_repo,
- revision: revision,
- path: path.presence || '.'
+ revision: GitalyClient.encode(revision),
+ path: path.present? ? GitalyClient.encode(path) : '.'
)
response = GitalyClient.call(@repository.storage, :commit_service, :get_tree_entries, request)
response.flat_map do |message|
message.entries.map do |gitaly_tree_entry|
- entry_path = gitaly_tree_entry.path.dup
Gitlab::Git::Tree.new(
id: gitaly_tree_entry.oid,
root_id: gitaly_tree_entry.root_oid,
type: gitaly_tree_entry.type.downcase,
mode: gitaly_tree_entry.mode.to_s(8),
- name: File.basename(entry_path),
- path: entry_path,
+ name: File.basename(gitaly_tree_entry.path),
+ path: GitalyClient.encode(gitaly_tree_entry.path),
+ flat_path: GitalyClient.encode(gitaly_tree_entry.flat_path),
commit_id: gitaly_tree_entry.commit_oid
)
end
@@ -100,15 +117,14 @@ module Gitlab
def last_commit_for_path(revision, path)
request = Gitaly::LastCommitForPathRequest.new(
repository: @gitaly_repo,
- revision: revision.force_encoding(Encoding::ASCII_8BIT),
- path: path.to_s.force_encoding(Encoding::ASCII_8BIT)
+ revision: GitalyClient.encode(revision),
+ path: GitalyClient.encode(path.to_s)
)
gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request).commit
return unless gitaly_commit
- commit = GitalyClient::Commit.new(@repository, gitaly_commit)
- Gitlab::Git::Commit.new(commit)
+ Gitlab::Git::Commit.new(@repository, gitaly_commit)
end
def between(from, to)
@@ -159,18 +175,39 @@ module Gitlab
def raw_blame(revision, path)
request = Gitaly::RawBlameRequest.new(
repository: @gitaly_repo,
- revision: revision,
- path: path
+ revision: GitalyClient.encode(revision),
+ path: GitalyClient.encode(path)
)
response = GitalyClient.call(@repository.storage, :commit_service, :raw_blame, request)
response.reduce("") { |memo, msg| memo << msg.data }
end
+ def find_commit(revision)
+ request = Gitaly::FindCommitRequest.new(
+ repository: @gitaly_repo,
+ revision: GitalyClient.encode(revision)
+ )
+
+ response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request)
+
+ response.commit
+ end
+
+ def patch(revision)
+ request = Gitaly::CommitPatchRequest.new(
+ repository: @gitaly_repo,
+ revision: GitalyClient.encode(revision)
+ )
+ response = GitalyClient.call(@repository.storage, :diff_service, :commit_patch, request)
+
+ response.sum(&:data)
+ end
+
private
def commit_diff_request_params(commit, options = {})
- parent_id = commit.parents[0]&.id || EMPTY_TREE_ID
+ parent_id = commit.parent_ids.first || EMPTY_TREE_ID
{
repository: @gitaly_repo,
@@ -183,8 +220,7 @@ module Gitlab
def consume_commits_response(response)
response.flat_map do |message|
message.commits.map do |gitaly_commit|
- commit = GitalyClient::Commit.new(@repository, gitaly_commit)
- Gitlab::Git::Commit.new(commit)
+ Gitlab::Git::Commit.new(@repository, gitaly_commit)
end
end
end
diff --git a/lib/gitlab/gitaly_client/diff.rb b/lib/gitlab/gitaly_client/diff.rb
index d459c9a88fb..54df6304865 100644
--- a/lib/gitlab/gitaly_client/diff.rb
+++ b/lib/gitlab/gitaly_client/diff.rb
@@ -7,13 +7,13 @@ module Gitlab
def initialize(params)
params.each do |key, val|
- public_send(:"#{key}=", val)
+ public_send(:"#{key}=", val) # rubocop:disable GitlabSecurity/PublicSend
end
end
def ==(other)
FIELDS.all? do |field|
- public_send(field) == other.public_send(field)
+ public_send(field) == other.public_send(field) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index b0f7548b7dc..8ef873d5848 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -16,8 +16,7 @@ module Gitlab
response.flat_map do |message|
message.branches.map do |branch|
- gitaly_commit = GitalyClient::Commit.new(@repository, branch.target)
- target_commit = Gitlab::Git::Commit.decorate(gitaly_commit)
+ target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target)
Gitlab::Git::Branch.new(@repository, branch.name, branch.target.id, target_commit)
end
end
@@ -71,6 +70,62 @@ module Gitlab
consume_tags_response(response)
end
+ def ref_exists?(ref_name)
+ request = Gitaly::RefExistsRequest.new(repository: @gitaly_repo, ref: GitalyClient.encode(ref_name))
+ response = GitalyClient.call(@storage, :ref_service, :ref_exists, request)
+ response.value
+ rescue GRPC::InvalidArgument => e
+ raise ArgumentError, e.message
+ end
+
+ def find_branch(branch_name)
+ request = Gitaly::FindBranchRequest.new(
+ repository: @gitaly_repo,
+ name: GitalyClient.encode(branch_name)
+ )
+
+ response = GitalyClient.call(@repository.storage, :ref_service, :find_branch, request)
+ branch = response.branch
+ return unless branch
+
+ target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
+ Gitlab::Git::Branch.new(@repository, encode!(branch.name.dup), branch.target_commit.id, target_commit)
+ end
+
+ def create_branch(ref, start_point)
+ request = Gitaly::CreateBranchRequest.new(
+ repository: @gitaly_repo,
+ name: GitalyClient.encode(ref),
+ start_point: GitalyClient.encode(start_point)
+ )
+
+ response = GitalyClient.call(@repository.storage, :ref_service, :create_branch, request)
+
+ case response.status
+ when :OK
+ branch = response.branch
+ target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target_commit)
+ Gitlab::Git::Branch.new(@repository, branch.name, branch.target_commit.id, target_commit)
+ when :ERR_INVALID
+ invalid_ref!("Invalid ref name")
+ when :ERR_EXISTS
+ invalid_ref!("Branch #{ref} already exists")
+ when :ERR_INVALID_START_POINT
+ invalid_ref!("Invalid reference #{start_point}")
+ else
+ raise "Unknown response status: #{response.status}"
+ end
+ end
+
+ def delete_branch(branch_name)
+ request = Gitaly::DeleteBranchRequest.new(
+ repository: @gitaly_repo,
+ name: GitalyClient.encode(branch_name)
+ )
+
+ GitalyClient.call(@repository.storage, :ref_service, :delete_branch, request)
+ end
+
private
def consume_refs_response(response)
@@ -102,8 +157,7 @@ module Gitlab
response.flat_map do |message|
message.tags.map do |gitaly_tag|
if gitaly_tag.target_commit.present?
- commit = GitalyClient::Commit.new(@repository, gitaly_tag.target_commit)
- gitaly_commit = Gitlab::Git::Commit.new(commit)
+ gitaly_commit = Gitlab::Git::Commit.decorate(@repository, gitaly_tag.target_commit)
end
Gitlab::Git::Tag.new(
@@ -141,7 +195,11 @@ module Gitlab
committer_email: response.commit_committer.email.dup
}
- Gitlab::Git::Commit.decorate(hash)
+ Gitlab::Git::Commit.decorate(@repository, hash)
+ end
+
+ def invalid_ref!(message)
+ raise Gitlab::Git::Repository::InvalidRef.new(message)
end
end
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 79ce784f2f2..177a1284f38 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -10,7 +10,7 @@ module Gitlab
def exists?
request = Gitaly::RepositoryExistsRequest.new(repository: @gitaly_repo)
- GitalyClient.call(@storage, :repository_service, :exists, request).exists
+ GitalyClient.call(@storage, :repository_service, :repository_exists, request).exists
end
def garbage_collect(create_bitmap)
@@ -32,6 +32,27 @@ module Gitlab
request = Gitaly::RepositorySizeRequest.new(repository: @gitaly_repo)
GitalyClient.call(@storage, :repository_service, :repository_size, request).size
end
+
+ def apply_gitattributes(revision)
+ request = Gitaly::ApplyGitattributesRequest.new(repository: @gitaly_repo, revision: revision)
+ GitalyClient.call(@storage, :repository_service, :apply_gitattributes, request)
+ end
+
+ def fetch_remote(remote, ssh_auth: nil, forced: false, no_tags: false)
+ request = Gitaly::FetchRemoteRequest.new(repository: @gitaly_repo, remote: remote, force: forced, no_tags: no_tags)
+
+ if ssh_auth&.ssh_import?
+ if ssh_auth.ssh_key_auth? && ssh_auth.ssh_private_key.present?
+ request.ssh_key = ssh_auth.ssh_private_key
+ end
+
+ if ssh_auth.ssh_known_hosts.present?
+ request.known_hosts = ssh_auth.ssh_known_hosts
+ end
+ end
+
+ GitalyClient.call(@storage, :repository_service, :fetch_remote, request)
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb
index f5a4c5493ef..8fc937496af 100644
--- a/lib/gitlab/gitaly_client/util.rb
+++ b/lib/gitlab/gitaly_client/util.rb
@@ -5,7 +5,9 @@ module Gitlab
def repository(repository_storage, relative_path)
Gitaly::Repository.new(
storage_name: repository_storage,
- relative_path: relative_path
+ relative_path: relative_path,
+ 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
end
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
index 8c80791e7c9..f330041cc00 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -11,7 +11,9 @@ module Gitlab
end
def create!
- project.public_send(project_association).find_or_create_by!(find_condition) do |record|
+ association = project.public_send(project_association) # rubocop:disable GitlabSecurity/PublicSend
+
+ association.find_or_create_by!(find_condition) do |record|
record.attributes = attributes
end
end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 7dbeec5b010..0550f9695bd 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -120,7 +120,7 @@ module Gitlab
def request(method, *args, &block)
sleep rate_limit_sleep_time if rate_limit_exceed?
- data = api.send(method, *args)
+ data = api.__send__(method, *args) # rubocop:disable GitlabSecurity/PublicSend
return data unless data.is_a?(Array)
last_response = api.last_response
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 266b1a6fece..b8c07460ebb 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -166,7 +166,7 @@ module Gitlab
def remove_branch(name)
project.repository.delete_branch(name)
- rescue Rugged::ReferenceError
+ rescue Gitlab::Git::Repository::DeleteBranchFailed
errors << { type: :remove_branch, name: name }
end
@@ -289,7 +289,7 @@ module Gitlab
opts.last[:page] = current_page(resource_type)
- client.public_send(resource_type, *opts) do |resources|
+ client.public_send(resource_type, *opts) do |resources| # rubocop:disable GitlabSecurity/PublicSend
yield resources
increment_page(resource_type)
end
diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb
index 86fb6c51765..f1007daab5d 100644
--- a/lib/gitlab/gitlab_import/client.rb
+++ b/lib/gitlab/gitlab_import/client.rb
@@ -71,7 +71,7 @@ module Gitlab
end
def config
- Gitlab.config.omniauth.providers.find{|provider| provider.name == "gitlab"}
+ Gitlab.config.omniauth.providers.find {|provider| provider.name == "gitlab"}
end
def gitlab_options
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 2d1ae6a5925..9bcc579278f 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -3,6 +3,7 @@
module Gitlab
module GonHelper
include WebpackHelper
+ include Gitlab::CurrentSettings
def add_gon_variables
gon.api_version = 'v4'
diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb
index e1d1724295a..025f826e65f 100644
--- a/lib/gitlab/gpg.rb
+++ b/lib/gitlab/gpg.rb
@@ -2,6 +2,8 @@ module Gitlab
module Gpg
extend self
+ MUTEX = Mutex.new
+
module CurrentKeyChain
extend self
@@ -37,26 +39,42 @@ module Gitlab
fingerprints = CurrentKeyChain.fingerprints_from_key(key)
GPGME::Key.find(:public, fingerprints).flat_map do |raw_key|
- raw_key.uids.map { |uid| { name: uid.name, email: uid.email } }
+ raw_key.uids.map { |uid| { name: uid.name, email: uid.email.downcase } }
end
end
end
- def using_tmp_keychain
- Dir.mktmpdir do |dir|
- @original_dirs ||= [GPGME::Engine.dirinfo('homedir')]
- @original_dirs.push(dir)
-
- GPGME::Engine.home_dir = dir
-
- return_value = yield
+ # Allows thread safe switching of temporary keychain files
+ #
+ # 1. The current thread may use nesting of temporary keychain
+ # 2. Another thread needs to wait for the lock to be released
+ def using_tmp_keychain(&block)
+ if MUTEX.locked? && MUTEX.owned?
+ optimistic_using_tmp_keychain(&block)
+ else
+ MUTEX.synchronize do
+ optimistic_using_tmp_keychain(&block)
+ end
+ end
+ end
- @original_dirs.pop
+ # 1. Returns the custom home directory if one has been set by calling
+ # `GPGME::Engine.home_dir=`
+ # 2. Returns the default home directory otherwise
+ def current_home_dir
+ GPGME::Engine.info.first.home_dir || GPGME::Engine.dirinfo('homedir')
+ end
- GPGME::Engine.home_dir = @original_dirs[-1]
+ private
- return_value
+ def optimistic_using_tmp_keychain
+ previous_dir = current_home_dir
+ Dir.mktmpdir do |dir|
+ GPGME::Engine.home_dir = dir
+ yield
end
+ ensure
+ GPGME::Engine.home_dir = previous_dir
end
end
end
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 55428b85207..86bd9f5b125 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -1,12 +1,15 @@
module Gitlab
module Gpg
class Commit
- attr_reader :commit
-
def initialize(commit)
@commit = commit
- @signature_text, @signed_text = commit.raw.signature(commit.project.repository)
+ @signature_text, @signed_text =
+ begin
+ Rugged::Commit.extract_signature(@commit.project.repository.rugged, @commit.sha)
+ rescue Rugged::OdbError
+ nil
+ end
end
def has_signature?
@@ -16,18 +19,20 @@ module Gitlab
def signature
return unless has_signature?
- cached_signature = GpgSignature.find_by(commit_sha: commit.sha)
- return cached_signature if cached_signature.present?
+ return @signature if @signature
- using_keychain do |gpg_key|
- create_cached_signature!(gpg_key)
- end
+ cached_signature = GpgSignature.find_by(commit_sha: @commit.sha)
+ return @signature = cached_signature if cached_signature.present?
+
+ @signature = create_cached_signature!
end
def update_signature!(cached_signature)
using_keychain do |gpg_key|
cached_signature.update_attributes!(attributes(gpg_key))
end
+
+ @signature = cached_signature
end
private
@@ -55,26 +60,39 @@ module Gitlab
end
end
- def create_cached_signature!(gpg_key)
- GpgSignature.create!(attributes(gpg_key))
+ def create_cached_signature!
+ using_keychain do |gpg_key|
+ GpgSignature.create!(attributes(gpg_key))
+ end
end
def attributes(gpg_key)
user_infos = user_infos(gpg_key)
+ verification_status = verification_status(gpg_key)
{
- commit_sha: commit.sha,
- project: commit.project,
+ commit_sha: @commit.sha,
+ project: @commit.project,
gpg_key: gpg_key,
gpg_key_primary_keyid: gpg_key&.primary_keyid || verified_signature.fingerprint,
gpg_key_user_name: user_infos[:name],
gpg_key_user_email: user_infos[:email],
- valid_signature: gpg_signature_valid_signature_value(gpg_key)
+ verification_status: verification_status
}
end
- def gpg_signature_valid_signature_value(gpg_key)
- !!(gpg_key && gpg_key.verified? && verified_signature.valid?)
+ def verification_status(gpg_key)
+ return :unknown_key unless gpg_key
+ return :unverified_key unless gpg_key.verified?
+ return :unverified unless verified_signature.valid?
+
+ if gpg_key.verified_and_belongs_to_email?(@commit.committer_email)
+ :verified
+ elsif gpg_key.user.all_emails.include?(@commit.committer_email)
+ :same_user_different_email
+ else
+ :other_user
+ end
end
def user_infos(gpg_key)
diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
index 3bb491120ba..e085eab26c9 100644
--- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
+++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
@@ -8,11 +8,9 @@ module Gitlab
def run
GpgSignature
.select(:id, :commit_sha, :project_id)
- .where('gpg_key_id IS NULL OR valid_signature = ?', false)
+ .where('gpg_key_id IS NULL OR verification_status <> ?', GpgSignature.verification_statuses[:verified])
.where(gpg_key_primary_keyid: @gpg_key.primary_keyid)
- .find_each do |gpg_signature|
- Gitlab::Gpg::Commit.new(gpg_signature.commit).update_signature!(gpg_signature)
- end
+ .find_each { |sig| sig.gpg_commit.update_signature!(sig) }
end
end
end
diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
new file mode 100644
index 00000000000..1e1fdabca93
--- /dev/null
+++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module GrapeLogging
+ module Formatters
+ class LogrageWithTimestamp
+ def call(severity, datetime, _, data)
+ time = data.delete :time
+ attributes = {
+ time: datetime.utc.iso8601(3),
+ severity: severity,
+ duration: time[:total],
+ db: time[:db],
+ view: time[:view]
+ }.merge(data)
+ ::Lograge.formatter.call(attributes) + "\n"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/health_checks/db_check.rb b/lib/gitlab/health_checks/db_check.rb
index fd94984f8a2..e27e16ddaf6 100644
--- a/lib/gitlab/health_checks/db_check.rb
+++ b/lib/gitlab/health_checks/db_check.rb
@@ -10,7 +10,7 @@ module Gitlab
'db_ping'
end
- def is_successful?(result)
+ def successful?(result)
result == '1'
end
diff --git a/lib/gitlab/health_checks/redis/cache_check.rb b/lib/gitlab/health_checks/redis/cache_check.rb
index a28658d42d4..0eb9b77634a 100644
--- a/lib/gitlab/health_checks/redis/cache_check.rb
+++ b/lib/gitlab/health_checks/redis/cache_check.rb
@@ -15,7 +15,7 @@ module Gitlab
'redis_cache_ping'
end
- def is_successful?(result)
+ def successful?(result)
result == 'PONG'
end
diff --git a/lib/gitlab/health_checks/redis/queues_check.rb b/lib/gitlab/health_checks/redis/queues_check.rb
index f97d50d3947..f322fe831b8 100644
--- a/lib/gitlab/health_checks/redis/queues_check.rb
+++ b/lib/gitlab/health_checks/redis/queues_check.rb
@@ -15,7 +15,7 @@ module Gitlab
'redis_queues_ping'
end
- def is_successful?(result)
+ def successful?(result)
result == 'PONG'
end
diff --git a/lib/gitlab/health_checks/redis/redis_check.rb b/lib/gitlab/health_checks/redis/redis_check.rb
index fe4e3c4a3ab..8ceb0a0aa46 100644
--- a/lib/gitlab/health_checks/redis/redis_check.rb
+++ b/lib/gitlab/health_checks/redis/redis_check.rb
@@ -11,7 +11,7 @@ module Gitlab
'redis_ping'
end
- def is_successful?(result)
+ def successful?(result)
result == 'PONG'
end
diff --git a/lib/gitlab/health_checks/redis/shared_state_check.rb b/lib/gitlab/health_checks/redis/shared_state_check.rb
index e3244392902..07e6f707998 100644
--- a/lib/gitlab/health_checks/redis/shared_state_check.rb
+++ b/lib/gitlab/health_checks/redis/shared_state_check.rb
@@ -15,7 +15,7 @@ module Gitlab
'redis_shared_state_ping'
end
- def is_successful?(result)
+ def successful?(result)
result == 'PONG'
end
diff --git a/lib/gitlab/health_checks/simple_abstract_check.rb b/lib/gitlab/health_checks/simple_abstract_check.rb
index f5026171ba4..96945ce5b20 100644
--- a/lib/gitlab/health_checks/simple_abstract_check.rb
+++ b/lib/gitlab/health_checks/simple_abstract_check.rb
@@ -5,7 +5,7 @@ module Gitlab
def readiness
check_result = check
- if is_successful?(check_result)
+ if successful?(check_result)
HealthChecks::Result.new(true)
elsif check_result.is_a?(Timeout::Error)
HealthChecks::Result.new(false, "#{human_name} check timed out")
@@ -16,10 +16,10 @@ module Gitlab
def metrics
result, elapsed = with_timing(&method(:check))
- Rails.logger.error("#{human_name} check returned unexpected result #{result}") unless is_successful?(result)
+ Rails.logger.error("#{human_name} check returned unexpected result #{result}") unless successful?(result)
[
metric("#{metric_prefix}_timeout", result.is_a?(Timeout::Error) ? 1 : 0),
- metric("#{metric_prefix}_success", is_successful?(result) ? 1 : 0),
+ metric("#{metric_prefix}_success", successful?(result) ? 1 : 0),
metric("#{metric_prefix}_latency_seconds", elapsed)
]
end
@@ -30,7 +30,7 @@ module Gitlab
raise NotImplementedError
end
- def is_successful?(result)
+ def successful?(result)
raise NotImplementedError
end
diff --git a/lib/gitlab/i18n/metadata_entry.rb b/lib/gitlab/i18n/metadata_entry.rb
new file mode 100644
index 00000000000..35d57459a3d
--- /dev/null
+++ b/lib/gitlab/i18n/metadata_entry.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module I18n
+ class MetadataEntry
+ attr_reader :entry_data
+
+ def initialize(entry_data)
+ @entry_data = entry_data
+ end
+
+ def expected_plurals
+ return nil unless plural_information
+
+ plural_information['nplurals'].to_i
+ end
+
+ private
+
+ def plural_information
+ return @plural_information if defined?(@plural_information)
+
+ if plural_line = entry_data[:msgstr].detect { |metadata_line| metadata_line.starts_with?('Plural-Forms: ') }
+ @plural_information = Hash[plural_line.scan(/(\w+)=([^;\n]+)/)]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/i18n/po_linter.rb b/lib/gitlab/i18n/po_linter.rb
new file mode 100644
index 00000000000..7d3ff8c7f58
--- /dev/null
+++ b/lib/gitlab/i18n/po_linter.rb
@@ -0,0 +1,214 @@
+module Gitlab
+ module I18n
+ class PoLinter
+ attr_reader :po_path, :translation_entries, :metadata_entry, :locale
+
+ VARIABLE_REGEX = /%{\w*}|%[a-z]/.freeze
+
+ def initialize(po_path, locale = I18n.locale.to_s)
+ @po_path = po_path
+ @locale = locale
+ end
+
+ def errors
+ @errors ||= validate_po
+ end
+
+ def validate_po
+ if parse_error = parse_po
+ return 'PO-syntax errors' => [parse_error]
+ end
+
+ validate_entries
+ end
+
+ def parse_po
+ entries = SimplePoParser.parse(po_path)
+
+ # The first entry is the metadata entry if there is one.
+ # This is an entry when empty `msgid`
+ if entries.first[:msgid].empty?
+ @metadata_entry = Gitlab::I18n::MetadataEntry.new(entries.shift)
+ else
+ return 'Missing metadata entry.'
+ end
+
+ @translation_entries = entries.map do |entry_data|
+ Gitlab::I18n::TranslationEntry.new(entry_data, metadata_entry.expected_plurals)
+ end
+
+ nil
+ rescue SimplePoParser::ParserError => e
+ @translation_entries = []
+ e.message
+ end
+
+ def validate_entries
+ errors = {}
+
+ translation_entries.each do |entry|
+ errors_for_entry = validate_entry(entry)
+ errors[join_message(entry.msgid)] = errors_for_entry if errors_for_entry.any?
+ end
+
+ errors
+ end
+
+ def validate_entry(entry)
+ errors = []
+
+ validate_flags(errors, entry)
+ validate_variables(errors, entry)
+ validate_newlines(errors, entry)
+ validate_number_of_plurals(errors, entry)
+ validate_unescaped_chars(errors, entry)
+
+ errors
+ end
+
+ def validate_unescaped_chars(errors, entry)
+ if entry.msgid_contains_unescaped_chars?
+ errors << 'contains unescaped `%`, escape it using `%%`'
+ end
+
+ if entry.plural_id_contains_unescaped_chars?
+ errors << 'plural id contains unescaped `%`, escape it using `%%`'
+ end
+
+ if entry.translations_contain_unescaped_chars?
+ errors << 'translation contains unescaped `%`, escape it using `%%`'
+ end
+ end
+
+ def validate_number_of_plurals(errors, entry)
+ return unless metadata_entry&.expected_plurals
+ return unless entry.translated?
+
+ if entry.has_plural? && entry.all_translations.size != metadata_entry.expected_plurals
+ errors << "should have #{metadata_entry.expected_plurals} "\
+ "#{'translations'.pluralize(metadata_entry.expected_plurals)}"
+ end
+ end
+
+ def validate_newlines(errors, entry)
+ if entry.msgid_contains_newlines?
+ errors << 'is defined over multiple lines, this breaks some tooling.'
+ end
+
+ if entry.plural_id_contains_newlines?
+ errors << 'plural is defined over multiple lines, this breaks some tooling.'
+ end
+
+ if entry.translations_contain_newlines?
+ errors << 'has translations defined over multiple lines, this breaks some tooling.'
+ end
+ end
+
+ def validate_variables(errors, entry)
+ if entry.has_singular_translation?
+ validate_variables_in_message(errors, entry.msgid, entry.singular_translation)
+ end
+
+ if entry.has_plural?
+ entry.plural_translations.each do |translation|
+ validate_variables_in_message(errors, entry.plural_id, translation)
+ end
+ end
+ end
+
+ def validate_variables_in_message(errors, message_id, message_translation)
+ message_id = join_message(message_id)
+ required_variables = message_id.scan(VARIABLE_REGEX)
+
+ validate_unnamed_variables(errors, required_variables)
+ validate_translation(errors, message_id, required_variables)
+ validate_variable_usage(errors, message_translation, required_variables)
+ end
+
+ def validate_translation(errors, message_id, used_variables)
+ variables = fill_in_variables(used_variables)
+
+ begin
+ Gitlab::I18n.with_locale(locale) do
+ translated = if message_id.include?('|')
+ FastGettext::Translation.s_(message_id)
+ else
+ FastGettext::Translation._(message_id)
+ end
+
+ translated % variables
+ end
+
+ # `sprintf` could raise an `ArgumentError` when invalid passing something
+ # other than a Hash when using named variables
+ #
+ # `sprintf` could raise `TypeError` when passing a wrong type when using
+ # unnamed variables
+ #
+ # FastGettext::Translation could raise `RuntimeError` (raised as a string),
+ # or as subclassess `NoTextDomainConfigured` & `InvalidFormat`
+ #
+ # `FastGettext::Translation` could raise `ArgumentError` as subclassess
+ # `InvalidEncoding`, `IllegalSequence` & `InvalidCharacter`
+ rescue ArgumentError, TypeError, RuntimeError => e
+ errors << "Failure translating to #{locale} with #{variables}: #{e.message}"
+ end
+ end
+
+ def fill_in_variables(variables)
+ if variables.empty?
+ []
+ elsif variables.any? { |variable| unnamed_variable?(variable) }
+ variables.map do |variable|
+ variable == '%d' ? Random.rand(1000) : Gitlab::Utils.random_string
+ end
+ else
+ variables.inject({}) do |hash, variable|
+ variable_name = variable[/\w+/]
+ hash[variable_name] = Gitlab::Utils.random_string
+ hash
+ end
+ end
+ end
+
+ def validate_unnamed_variables(errors, variables)
+ if variables.size > 1 && variables.any? { |variable_name| unnamed_variable?(variable_name) }
+ errors << 'is combining multiple unnamed variables'
+ end
+ end
+
+ def validate_variable_usage(errors, translation, required_variables)
+ translation = join_message(translation)
+
+ # We don't need to validate when the message is empty.
+ # In this case we fall back to the default, which has all the the
+ # required variables.
+ return if translation.empty?
+
+ found_variables = translation.scan(VARIABLE_REGEX)
+
+ missing_variables = required_variables - found_variables
+ if missing_variables.any?
+ errors << "<#{translation}> is missing: [#{missing_variables.to_sentence}]"
+ end
+
+ unknown_variables = found_variables - required_variables
+ if unknown_variables.any?
+ errors << "<#{translation}> is using unknown variables: [#{unknown_variables.to_sentence}]"
+ end
+ end
+
+ def unnamed_variable?(variable_name)
+ !variable_name.start_with?('%{')
+ end
+
+ def validate_flags(errors, entry)
+ errors << "is marked #{entry.flag}" if entry.flag
+ end
+
+ def join_message(message)
+ Array(message).join
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/i18n/translation_entry.rb b/lib/gitlab/i18n/translation_entry.rb
new file mode 100644
index 00000000000..e6c95afca7e
--- /dev/null
+++ b/lib/gitlab/i18n/translation_entry.rb
@@ -0,0 +1,92 @@
+module Gitlab
+ module I18n
+ class TranslationEntry
+ PERCENT_REGEX = /(?:^|[^%])%(?!{\w*}|[a-z%])/.freeze
+
+ attr_reader :nplurals, :entry_data
+
+ def initialize(entry_data, nplurals)
+ @entry_data = entry_data
+ @nplurals = nplurals
+ end
+
+ def msgid
+ entry_data[:msgid]
+ end
+
+ def plural_id
+ entry_data[:msgid_plural]
+ end
+
+ def has_plural?
+ plural_id.present?
+ end
+
+ def singular_translation
+ all_translations.first if has_singular_translation?
+ end
+
+ def all_translations
+ @all_translations ||= entry_data.fetch_values(*translation_keys)
+ .reject(&:empty?)
+ end
+
+ def translated?
+ all_translations.any?
+ end
+
+ def plural_translations
+ return [] unless has_plural?
+ return [] unless translated?
+
+ @plural_translations ||= if has_singular_translation?
+ all_translations.drop(1)
+ else
+ all_translations
+ end
+ end
+
+ def flag
+ entry_data[:flag]
+ end
+
+ def has_singular_translation?
+ nplurals > 1 || !has_plural?
+ end
+
+ def msgid_contains_newlines?
+ msgid.is_a?(Array)
+ end
+
+ def plural_id_contains_newlines?
+ plural_id.is_a?(Array)
+ end
+
+ def translations_contain_newlines?
+ all_translations.any? { |translation| translation.is_a?(Array) }
+ end
+
+ def msgid_contains_unescaped_chars?
+ contains_unescaped_chars?(msgid)
+ end
+
+ def plural_id_contains_unescaped_chars?
+ contains_unescaped_chars?(plural_id)
+ end
+
+ def translations_contain_unescaped_chars?
+ all_translations.any? { |translation| contains_unescaped_chars?(translation) }
+ end
+
+ def contains_unescaped_chars?(string)
+ string =~ PERCENT_REGEX
+ end
+
+ private
+
+ def translation_keys
+ @translation_keys ||= entry_data.keys.select { |key| key.to_s =~ /\Amsgstr(\[\d+\])?\z/ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index 3470a09eaf0..50ee879129c 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.1.8'.freeze
+ VERSION = '0.2.0'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/attributes_finder.rb b/lib/gitlab/import_export/attributes_finder.rb
index d230de781d5..56042ddecbf 100644
--- a/lib/gitlab/import_export/attributes_finder.rb
+++ b/lib/gitlab/import_export/attributes_finder.rb
@@ -1,7 +1,6 @@
module Gitlab
module ImportExport
class AttributesFinder
-
def initialize(included_attributes:, excluded_attributes:, methods:)
@included_attributes = included_attributes || {}
@excluded_attributes = excluded_attributes || {}
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
index ffd17118c91..989342389bc 100644
--- a/lib/gitlab/import_export/file_importer.rb
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -47,12 +47,16 @@ module Gitlab
end
def remove_symlinks!
- Dir["#{@shared.export_path}/**/*"].each do |path|
+ extracted_files.each do |path|
FileUtils.rm(path) if File.lstat(path).symlink?
end
true
end
+
+ def extracted_files
+ Dir.glob("#{@shared.export_path}/**/*", File::FNM_DOTMATCH).reject { |f| f =~ /.*\/\.{1,2}$/ }
+ end
end
end
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index c5c05bfe2fb..2171c6c7bbb 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -3,18 +3,22 @@ project_tree:
- labels:
:priorities
- milestones:
- - :events
+ - events:
+ - :push_event_payload
- issues:
- - :events
+ - events:
+ - :push_event_payload
- :timelogs
- notes:
- :author
- - :events
+ - events:
+ - :push_event_payload
- label_links:
- label:
:priorities
- milestone:
- - :events
+ - events:
+ - :push_event_payload
- snippets:
- :award_emoji
- notes:
@@ -25,23 +29,28 @@ project_tree:
- merge_requests:
- notes:
- :author
- - :events
+ - events:
+ - :push_event_payload
- merge_request_diff:
- :merge_request_diff_commits
- :merge_request_diff_files
- - :events
+ - events:
+ - :push_event_payload
- :timelogs
- label_links:
- label:
:priorities
- milestone:
- - :events
+ - events:
+ - :push_event_payload
- pipelines:
- notes:
- :author
- - :events
+ - events:
+ - :push_event_payload
- :stages
- :statuses
+ - :auto_devops
- :triggers
- :pipeline_schedules
- :services
@@ -90,6 +99,7 @@ excluded_attributes:
- :last_activity_at
- :last_repository_updated_at
- :last_repository_check_at
+ - :storage_version
snippets:
- :expired_at
merge_request_diff:
@@ -107,6 +117,9 @@ excluded_attributes:
statuses:
- :trace
- :token
+ - :when
+ push_event_payload:
+ - :event_id
methods:
labels:
@@ -123,5 +136,11 @@ methods:
- :utf8_diff
merge_requests:
- :diff_head_sha
+ - :source_branch_sha
+ - :target_branch_sha
project:
- :description_html
+ events:
+ - :action
+ push_event_payload:
+ - :action
diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb
index c20adc20bfd..81a213e8321 100644
--- a/lib/gitlab/import_export/merge_request_parser.rb
+++ b/lib/gitlab/import_export/merge_request_parser.rb
@@ -30,7 +30,7 @@ module Gitlab
end
def branch_exists?(branch_name)
- @project.repository.branch_exists?(branch_name)
+ @project.repository.raw.branch_exists?(branch_name)
end
def fork_merge_request?
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 84ab1977dfa..3bc095a99a9 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -1,11 +1,16 @@
module Gitlab
module ImportExport
class ProjectTreeRestorer
+ # Relations which cannot have both group_id and project_id at the same time
+ RESTRICT_PROJECT_AND_GROUP = %i(milestones).freeze
+
def initialize(user:, shared:, project:)
@path = File.join(shared.export_path, 'project.json')
@user = user
@shared = shared
@project = project
+ @project_id = project.id
+ @saved = true
end
def restore
@@ -19,8 +24,10 @@ module Gitlab
@project_members = @tree_hash.delete('project_members')
- ActiveRecord::Base.no_touching do
- create_relations
+ ActiveRecord::Base.uncached do
+ ActiveRecord::Base.no_touching do
+ create_relations
+ end
end
rescue => e
@shared.error(e)
@@ -45,21 +52,24 @@ module Gitlab
# the configuration yaml file too.
# Finally, it updates each attribute in the newly imported project.
def create_relations
- saved = []
default_relation_list.each do |relation|
- next unless relation.is_a?(Hash) || @tree_hash[relation.to_s].present?
+ if relation.is_a?(Hash)
+ create_sub_relations(relation, @tree_hash)
+ elsif @tree_hash[relation.to_s].present?
+ save_relation_hash(@tree_hash[relation.to_s], relation)
+ end
+ end
- create_sub_relations(relation, @tree_hash) if relation.is_a?(Hash)
+ @saved
+ end
- relation_key = relation.is_a?(Hash) ? relation.keys.first : relation
- relation_hash_list = @tree_hash[relation_key.to_s]
+ def save_relation_hash(relation_hash_batch, relation_key)
+ relation_hash = create_relation(relation_key, relation_hash_batch)
- next unless relation_hash_list
+ @saved = false unless restored_project.append_or_update_attribute(relation_key, relation_hash)
- relation_hash = create_relation(relation_key, relation_hash_list)
- saved << restored_project.append_or_update_attribute(relation_key, relation_hash)
- end
- saved.all?
+ # Restore the project again, extra query that skips holding the AR objects in memory
+ @restored_project = Project.find(@project_id)
end
def default_relation_list
@@ -90,20 +100,42 @@ module Gitlab
# issue, finds any subrelations such as notes, creates them and assign them back to the hash
#
# Recursively calls this method if the sub-relation is a hash containing more sub-relations
- def create_sub_relations(relation, tree_hash)
+ def create_sub_relations(relation, tree_hash, save: true)
relation_key = relation.keys.first.to_s
return if tree_hash[relation_key].blank?
- [tree_hash[relation_key]].flatten.each do |relation_item|
- relation.values.flatten.each do |sub_relation|
- # We just use author to get the user ID, do not attempt to create an instance.
- next if sub_relation == :author
+ tree_array = [tree_hash[relation_key]].flatten
+
+ # Avoid keeping a possible heavy object in memory once we are done with it
+ while relation_item = tree_array.shift
+ # The transaction at this level is less speedy than one single transaction
+ # But we can't have it in the upper level or GC won't get rid of the AR objects
+ # after we save the batch.
+ Project.transaction do
+ process_sub_relation(relation, relation_item)
+
+ # For every subrelation that hangs from Project, save the associated records alltogether
+ # This effectively batches all records per subrelation item, only keeping those in memory
+ # We have to keep in mind that more batch granularity << Memory, but >> Slowness
+ if save
+ save_relation_hash([relation_item], relation_key)
+ tree_hash[relation_key].delete(relation_item)
+ end
+ end
+ end
- create_sub_relations(sub_relation, relation_item) if sub_relation.is_a?(Hash)
+ tree_hash.delete(relation_key) if save
+ end
- relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation)
- relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank?
- end
+ def process_sub_relation(relation, relation_item)
+ relation.values.flatten.each do |sub_relation|
+ # We just use author to get the user ID, do not attempt to create an instance.
+ next if sub_relation == :author
+
+ create_sub_relations(sub_relation, relation_item, save: false) if sub_relation.is_a?(Hash)
+
+ relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation)
+ relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank?
end
end
@@ -120,17 +152,25 @@ module Gitlab
def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
- relation_hash: parsed_relation_hash(relation_hash),
+ relation_hash: parsed_relation_hash(relation_hash, relation.to_sym),
members_mapper: members_mapper,
user: @user,
- project: restored_project)
+ project: @restored_project)
end.compact
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
end
- def parsed_relation_hash(relation_hash)
- relation_hash.merge!('group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id)
+ def parsed_relation_hash(relation_hash, relation_type)
+ if RESTRICT_PROJECT_AND_GROUP.include?(relation_type)
+ params = {}
+ params['group_id'] = restored_project.group.try(:id) if relation_hash['group_id']
+ params['project_id'] = restored_project.id if relation_hash['project_id']
+ else
+ params = { 'group_id' => restored_project.group.try(:id), 'project_id' => restored_project.id }
+ end
+
+ relation_hash.merge(params)
end
end
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 20580459046..380b336395d 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -14,6 +14,7 @@ module Gitlab
create_access_levels: 'ProtectedTag::CreateAccessLevel',
labels: :project_labels,
priorities: :label_priorities,
+ auto_devops: :project_auto_devops,
label: :project_label }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id].freeze
@@ -69,7 +70,6 @@ module Gitlab
reset_tokens!
remove_encrypted_attributes!
- @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
set_st_diff_commits if @relation_name == :merge_request_diff
set_diff if @relation_name == :merge_request_diff_files
end
diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb
index 5d6de8bc475..9fd0b709ef2 100644
--- a/lib/gitlab/import_export/shared.rb
+++ b/lib/gitlab/import_export/shared.rb
@@ -16,7 +16,7 @@ module Gitlab
error_out(error.message, caller[0].dup)
@errors << error.message
# Debug:
- Rails.logger.error(error.backtrace)
+ Rails.logger.error(error.backtrace.join("\n"))
end
private
diff --git a/lib/gitlab/issuables_count_for_state.rb b/lib/gitlab/issuables_count_for_state.rb
new file mode 100644
index 00000000000..505810964bc
--- /dev/null
+++ b/lib/gitlab/issuables_count_for_state.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ # Class for counting and caching the number of issuables per state.
+ class IssuablesCountForState
+ # The name of the RequestStore cache key.
+ CACHE_KEY = :issuables_count_for_state
+
+ # The state values that can be safely casted to a Symbol.
+ STATES = %w[opened closed merged all].freeze
+
+ # finder - The finder class to use for retrieving the issuables.
+ def initialize(finder)
+ @finder = finder
+ @cache =
+ if RequestStore.active?
+ RequestStore[CACHE_KEY] ||= initialize_cache
+ else
+ initialize_cache
+ end
+ end
+
+ def for_state_or_opened(state = nil)
+ self[state || :opened]
+ end
+
+ # Returns the count for the given state.
+ #
+ # state - The name of the state as either a String or a Symbol.
+ #
+ # Returns an Integer.
+ def [](state)
+ state = state.to_sym if cast_state_to_symbol?(state)
+
+ cache_for_finder[state] || 0
+ end
+
+ private
+
+ def cache_for_finder
+ @cache[@finder]
+ end
+
+ def cast_state_to_symbol?(state)
+ state.is_a?(String) && STATES.include?(state)
+ end
+
+ def initialize_cache
+ Hash.new { |hash, finder| hash[finder] = finder.count_by_state }
+ end
+ end
+end
diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb
index 208f0e1bbea..4d6bbda15f3 100644
--- a/lib/gitlab/job_waiter.rb
+++ b/lib/gitlab/job_waiter.rb
@@ -1,12 +1,31 @@
module Gitlab
# JobWaiter can be used to wait for a number of Sidekiq jobs to complete.
+ #
+ # Its use requires the cooperation of the sidekiq jobs themselves. Set up the
+ # waiter, then start the jobs, passing them its `key`. Their `perform` methods
+ # should look like:
+ #
+ # def perform(args, notify_key)
+ # # do work
+ # ensure
+ # ::Gitlab::JobWaiter.notify(notify_key, jid)
+ # end
+ #
+ # The JobWaiter blocks popping items from a Redis array. All the sidekiq jobs
+ # push to that array when done. Once the waiter has popped `count` items, it
+ # knows all the jobs are done.
class JobWaiter
- # The sleep interval between checking keys, in seconds.
- INTERVAL = 0.1
+ def self.notify(key, jid)
+ Gitlab::Redis::SharedState.with { |redis| redis.lpush(key, jid) }
+ end
+
+ attr_reader :key, :jobs_remaining, :finished
- # jobs - The job IDs to wait for.
- def initialize(jobs)
- @jobs = jobs
+ # jobs_remaining - the number of jobs left to wait for
+ def initialize(jobs_remaining)
+ @key = "gitlab:job_waiter:#{SecureRandom.uuid}"
+ @jobs_remaining = jobs_remaining
+ @finished = []
end
# Waits for all the jobs to be completed.
@@ -15,13 +34,33 @@ module Gitlab
# ensures we don't indefinitely block a caller in case a job takes
# long to process, or is never processed.
def wait(timeout = 10)
- start = Time.current
+ deadline = Time.now.utc + timeout
+
+ Gitlab::Redis::SharedState.with do |redis|
+ # Fallback key expiry: allow a long grace period to reduce the chance of
+ # a job pushing to an expired key and recreating it
+ redis.expire(key, [timeout * 2, 10.minutes.to_i].max)
+
+ while jobs_remaining > 0
+ # Redis will not take fractional seconds. Prefer waiting too long over
+ # not waiting long enough
+ seconds_left = (deadline - Time.now.utc).ceil
- while (Time.current - start) <= timeout
- break if SidekiqStatus.all_completed?(@jobs)
+ # Redis interprets 0 as "wait forever", so skip the final `blpop` call
+ break if seconds_left <= 0
- sleep(INTERVAL) # to not overload Redis too much.
+ list, jid = redis.blpop(key, timeout: seconds_left)
+ break unless list && jid # timed out
+
+ @finished << jid
+ @jobs_remaining -= 1
+ end
+
+ # All jobs have finished, so expire the key immediately
+ redis.expire(key, 0) if jobs_remaining == 0
end
+
+ finished
end
end
end
diff --git a/lib/gitlab/key_fingerprint.rb b/lib/gitlab/key_fingerprint.rb
deleted file mode 100644
index d9a79f7c291..00000000000
--- a/lib/gitlab/key_fingerprint.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-module Gitlab
- class KeyFingerprint
- attr_reader :key, :ssh_key
-
- # Unqualified MD5 fingerprint for compatibility
- delegate :fingerprint, to: :ssh_key, allow_nil: true
-
- def initialize(key)
- @key = key
-
- @ssh_key =
- begin
- Net::SSH::KeyFactory.load_data_public_key(key)
- rescue Net::SSH::Exception, NotImplementedError
- end
- end
-
- def valid?
- ssh_key.present?
- end
-
- def type
- return unless valid?
-
- parts = ssh_key.ssh_type.split('-')
- parts.shift if parts[0] == 'ssh'
-
- parts[0].upcase
- end
-
- def bits
- return unless valid?
-
- case type
- when 'RSA'
- ssh_key.n.num_bits
- when 'DSS', 'DSA'
- ssh_key.p.num_bits
- when 'ECDSA'
- ssh_key.group.order.num_bits
- when 'ED25519'
- 256
- else
- raise "Unsupported key type: #{type}"
- end
- end
- end
-end
diff --git a/lib/gitlab/lazy.rb b/lib/gitlab/lazy.rb
index 2a659ae4c74..99594577141 100644
--- a/lib/gitlab/lazy.rb
+++ b/lib/gitlab/lazy.rb
@@ -16,7 +16,7 @@ module Gitlab
def method_missing(name, *args, &block)
__evaluate__
- @result.__send__(name, *args, &block)
+ @result.__send__(name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def respond_to_missing?(name, include_private = false)
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index 8867a91c244..cd7e4ca7b7e 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -73,7 +73,7 @@ module Gitlab
private
def user_options(field, value, limit)
- options = { attributes: user_attributes }
+ options = { attributes: Gitlab::LDAP::Person.ldap_attributes(config).compact.uniq }
options[:size] = limit if limit
if field.to_sym == :dn
@@ -99,10 +99,6 @@ module Gitlab
filter
end
end
-
- def user_attributes
- %W(#{config.uid} cn dn) + config.attributes['username'] + config.attributes['email']
- end
end
end
end
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
index 95378e5a769..4fbc5fa5262 100644
--- a/lib/gitlab/ldap/auth_hash.rb
+++ b/lib/gitlab/ldap/auth_hash.rb
@@ -17,7 +17,7 @@ module Gitlab
value = value.first if value
break if value.present?
end
-
+
return super unless value
Gitlab::Utils.force_utf8(value)
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
index 43eb73250b7..4d6f8ac79de 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -21,6 +21,15 @@ module Gitlab
adapter.dn_matches_filter?(dn, AD_USER_DISABLED)
end
+ def self.ldap_attributes(config)
+ [
+ 'dn', # Used in `dn`
+ config.uid, # Used in `uid`
+ *config.attributes['name'], # Used in `name`
+ *config.attributes['email'] # Used in `email`
+ ]
+ end
+
def initialize(entry, provider)
Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
@entry = entry
@@ -32,7 +41,7 @@ module Gitlab
end
def uid
- entry.send(config.uid).first
+ entry.public_send(config.uid).first # rubocop:disable GitlabSecurity/PublicSend
end
def username
@@ -65,7 +74,7 @@ module Gitlab
return nil unless selected_attr
- entry.public_send(selected_attr)
+ entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index 39180dc17d9..3bf27b37ae6 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -36,7 +36,7 @@ module Gitlab
end
def find_by_email
- ::User.find_by(email: auth_hash.email.downcase) if auth_hash.has_email?
+ ::User.find_by(email: auth_hash.email.downcase) if auth_hash.has_attribute?(:email)
end
def update_user_attributes
@@ -60,7 +60,7 @@ module Gitlab
ldap_config.block_auto_created_users
end
- def sync_email_from_provider?
+ def sync_profile_from_provider?
true
end
diff --git a/lib/gitlab/logger.rb b/lib/gitlab/logger.rb
index 59b21149a9a..6bffd410ed0 100644
--- a/lib/gitlab/logger.rb
+++ b/lib/gitlab/logger.rb
@@ -14,13 +14,9 @@ module Gitlab
def self.read_latest
path = Rails.root.join("log", file_name)
- self.build unless File.exist?(path)
- tail_output, _ = Gitlab::Popen.popen(%W(tail -n 2000 #{path}))
- tail_output.split("\n")
- end
- def self.read_latest_for(filename)
- path = Rails.root.join("log", filename)
+ return [] unless File.readable?(path)
+
tail_output, _ = Gitlab::Popen.popen(%W(tail -n 2000 #{path}))
tail_output.split("\n")
end
diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb
index 699d8b9fc07..306923902e0 100644
--- a/lib/gitlab/markdown/pipeline.rb
+++ b/lib/gitlab/markdown/pipeline.rb
@@ -23,7 +23,7 @@ module Gitlab
define_method(meth) do |text, context|
context = transform_context(context)
- html_pipeline.send(meth, text, context)
+ html_pipeline.__send__(meth, text, context) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb
index d7c56463aac..7b06bb953aa 100644
--- a/lib/gitlab/metrics/influx_db.rb
+++ b/lib/gitlab/metrics/influx_db.rb
@@ -1,7 +1,7 @@
module Gitlab
module Metrics
module InfluxDb
- extend Gitlab::CurrentSettings
+ include Gitlab::CurrentSettings
extend self
MUTEX = Mutex.new
diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb
index 5d2d7d0026c..63c3372da51 100644
--- a/lib/gitlab/middleware/rails_queue_duration.rb
+++ b/lib/gitlab/middleware/rails_queue_duration.rb
@@ -8,7 +8,7 @@ module Gitlab
def initialize(app)
@app = app
end
-
+
def call(env)
trans = Gitlab::Metrics.current_transaction
proxy_start = env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence
diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb
index 6105d165810..6aecf63231f 100644
--- a/lib/gitlab/middleware/webpack_proxy.rb
+++ b/lib/gitlab/middleware/webpack_proxy.rb
@@ -1,6 +1,7 @@
# This Rack middleware is intended to proxy the webpack assets directory to the
# webpack-dev-server. It is only intended for use in development.
+# :nocov:
module Gitlab
module Middleware
class WebpackProxy < Rack::Proxy
@@ -22,3 +23,4 @@ module Gitlab
end
end
end
+# :nocov:
diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
index 7d6911a1ab3..1f331b1e91d 100644
--- a/lib/gitlab/o_auth/auth_hash.rb
+++ b/lib/gitlab/o_auth/auth_hash.rb
@@ -32,8 +32,21 @@ module Gitlab
@password ||= Gitlab::Utils.force_utf8(Devise.friendly_token[0, 8].downcase)
end
- def has_email?
- get_info(:email).present?
+ def location
+ location = get_info(:address)
+ if location.is_a?(Hash)
+ [location.locality.presence, location.country.presence].compact.join(', ')
+ else
+ location
+ end
+ end
+
+ def has_attribute?(attribute)
+ if attribute == :location
+ get_info(:address).present?
+ else
+ get_info(attribute).present?
+ end
end
private
diff --git a/lib/gitlab/o_auth/session.rb b/lib/gitlab/o_auth/session.rb
index f33bfd0bd0e..30739f2a2c5 100644
--- a/lib/gitlab/o_auth/session.rb
+++ b/lib/gitlab/o_auth/session.rb
@@ -1,3 +1,4 @@
+# :nocov:
module Gitlab
module OAuth
module Session
@@ -15,3 +16,4 @@ module Gitlab
end
end
end
+# :nocov:
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index e8330917e91..7704bf715e4 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -12,7 +12,7 @@ module Gitlab
def initialize(auth_hash)
self.auth_hash = auth_hash
- update_email
+ update_profile if sync_profile_from_provider?
end
def persisted?
@@ -184,20 +184,30 @@ module Gitlab
}
end
- def sync_email_from_provider?
- auth_hash.provider.to_s == Gitlab.config.omniauth.sync_email_from_provider.to_s
+ def sync_profile_from_provider?
+ providers = Gitlab.config.omniauth.sync_profile_from_provider
+
+ if providers.is_a?(Array)
+ providers.include?(auth_hash.provider)
+ else
+ providers
+ end
end
- def update_email
- if auth_hash.has_email? && sync_email_from_provider?
- if persisted?
- gl_user.skip_reconfirmation!
- gl_user.email = auth_hash.email
- end
+ def update_profile
+ user_synced_attributes_metadata = gl_user.user_synced_attributes_metadata || gl_user.build_user_synced_attributes_metadata
- gl_user.external_email = true
- gl_user.email_provider = auth_hash.provider
+ UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key|
+ if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key)
+ gl_user[key] = auth_hash.public_send(key) # rubocop:disable GitlabSecurity/PublicSend
+ user_synced_attributes_metadata.set_attribute_synced(key, true)
+ else
+ user_synced_attributes_metadata.set_attribute_synced(key, false)
+ end
end
+
+ user_synced_attributes_metadata.provider = auth_hash.provider
+ gl_user.user_synced_attributes_metadata = user_synced_attributes_metadata
end
def log
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 894bd5efae5..7c02c9c5c48 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -26,6 +26,7 @@ module Gitlab
apple-touch-icon.png
assets
autocomplete
+ boards
ci
dashboard
deploy.html
diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb
index 56112ec2301..e73245b82c1 100644
--- a/lib/gitlab/performance_bar.rb
+++ b/lib/gitlab/performance_bar.rb
@@ -1,6 +1,6 @@
module Gitlab
module PerformanceBar
- include Gitlab::CurrentSettings
+ extend Gitlab::CurrentSettings
ALLOWED_USER_IDS_KEY = 'performance_bar_allowed_user_ids:v2'.freeze
EXPIRY_TIME = 5.minutes
diff --git a/lib/gitlab/polling_interval.rb b/lib/gitlab/polling_interval.rb
index f0c50584f07..4780675a492 100644
--- a/lib/gitlab/polling_interval.rb
+++ b/lib/gitlab/polling_interval.rb
@@ -1,6 +1,6 @@
module Gitlab
class PollingInterval
- include Gitlab::CurrentSettings
+ extend Gitlab::CurrentSettings
HEADER_NAME = 'Poll-Interval'.freeze
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
index cf461adf697..732fbf68dad 100644
--- a/lib/gitlab/project_template.rb
+++ b/lib/gitlab/project_template.rb
@@ -25,7 +25,9 @@ module Gitlab
end
TEMPLATES_TABLE = [
- ProjectTemplate.new('rails', 'Ruby on Rails')
+ ProjectTemplate.new('rails', 'Ruby on Rails'),
+ ProjectTemplate.new('spring', 'Spring'),
+ ProjectTemplate.new('express', 'NodeJS Express')
].freeze
class << self
diff --git a/lib/gitlab/prometheus/queries/matched_metrics_query.rb b/lib/gitlab/prometheus/queries/matched_metrics_query.rb
index d4894c87f8d..4c3edccc71a 100644
--- a/lib/gitlab/prometheus/queries/matched_metrics_query.rb
+++ b/lib/gitlab/prometheus/queries/matched_metrics_query.rb
@@ -42,13 +42,13 @@ module Gitlab
lookup = series.each_slice(MAX_QUERY_ITEMS).flat_map do |batched_series|
client_series(*batched_series, start: timeframe_start, stop: timeframe_end)
- .select(&method(:has_matching_label))
+ .select(&method(:has_matching_label?))
.map { |series_info| [series_info['__name__'], true] }
end
lookup.to_h
end
- def has_matching_label(series_info)
+ def has_matching_label?(series_info)
series_info.key?('environment')
end
diff --git a/lib/gitlab/protocol_access.rb b/lib/gitlab/protocol_access.rb
index 21aefc884be..09fa14764e6 100644
--- a/lib/gitlab/protocol_access.rb
+++ b/lib/gitlab/protocol_access.rb
@@ -1,5 +1,7 @@
module Gitlab
module ProtocolAccess
+ extend Gitlab::CurrentSettings
+
def self.allowed?(protocol)
if protocol == 'web'
true
diff --git a/lib/gitlab/recaptcha.rb b/lib/gitlab/recaptcha.rb
index 4bc76ea033f..c463dd487a0 100644
--- a/lib/gitlab/recaptcha.rb
+++ b/lib/gitlab/recaptcha.rb
@@ -1,5 +1,7 @@
module Gitlab
module Recaptcha
+ extend Gitlab::CurrentSettings
+
def self.load_configurations!
if current_application_settings.recaptcha_enabled
::Recaptcha.configure do |config|
diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb
index b0da516ff83..9bf019b72e6 100644
--- a/lib/gitlab/redis/cache.rb
+++ b/lib/gitlab/redis/cache.rb
@@ -7,9 +7,6 @@ module Gitlab
CACHE_NAMESPACE = 'cache:gitlab'.freeze
DEFAULT_REDIS_CACHE_URL = 'redis://localhost:6380'.freeze
REDIS_CACHE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_CACHE_CONFIG_FILE'.freeze
- if defined?(::Rails) && ::Rails.root.present?
- DEFAULT_REDIS_CACHE_CONFIG_FILE_NAME = ::Rails.root.join('config', 'redis.cache.yml').freeze
- end
class << self
def default_url
@@ -22,7 +19,7 @@ module Gitlab
return file_name unless file_name.nil?
# otherwise, if config files exists for this class, use it
- file_name = File.expand_path(DEFAULT_REDIS_CACHE_CONFIG_FILE_NAME, __dir__)
+ file_name = config_file_path('redis.cache.yml')
return file_name if File.file?(file_name)
# this will force use of DEFAULT_REDIS_QUEUES_URL when config file is absent
diff --git a/lib/gitlab/redis/queues.rb b/lib/gitlab/redis/queues.rb
index f9249d05565..e1695aafbeb 100644
--- a/lib/gitlab/redis/queues.rb
+++ b/lib/gitlab/redis/queues.rb
@@ -8,9 +8,6 @@ module Gitlab
MAILROOM_NAMESPACE = 'mail_room:gitlab'.freeze
DEFAULT_REDIS_QUEUES_URL = 'redis://localhost:6381'.freeze
REDIS_QUEUES_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_QUEUES_CONFIG_FILE'.freeze
- if defined?(::Rails) && ::Rails.root.present?
- DEFAULT_REDIS_QUEUES_CONFIG_FILE_NAME = ::Rails.root.join('config', 'redis.queues.yml').freeze
- end
class << self
def default_url
@@ -23,7 +20,7 @@ module Gitlab
return file_name if file_name
# otherwise, if config files exists for this class, use it
- file_name = File.expand_path(DEFAULT_REDIS_QUEUES_CONFIG_FILE_NAME, __dir__)
+ file_name = config_file_path('redis.queues.yml')
return file_name if File.file?(file_name)
# this will force use of DEFAULT_REDIS_QUEUES_URL when config file is absent
diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb
index 395dcf082da..10bec7a90da 100644
--- a/lib/gitlab/redis/shared_state.rb
+++ b/lib/gitlab/redis/shared_state.rb
@@ -7,9 +7,6 @@ module Gitlab
SESSION_NAMESPACE = 'session:gitlab'.freeze
DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze
REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze
- if defined?(::Rails) && ::Rails.root.present?
- DEFAULT_REDIS_SHARED_STATE_CONFIG_FILE_NAME = ::Rails.root.join('config', 'redis.shared_state.yml').freeze
- end
class << self
def default_url
@@ -22,7 +19,7 @@ module Gitlab
return file_name if file_name
# otherwise, if config files exists for this class, use it
- file_name = File.expand_path(DEFAULT_REDIS_SHARED_STATE_CONFIG_FILE_NAME, __dir__)
+ file_name = config_file_path('redis.shared_state.yml')
return file_name if File.file?(file_name)
# this will force use of DEFAULT_REDIS_SHARED_STATE_URL when config file is absent
diff --git a/lib/gitlab/redis/wrapper.rb b/lib/gitlab/redis/wrapper.rb
index c43b37dde74..8ad06480575 100644
--- a/lib/gitlab/redis/wrapper.rb
+++ b/lib/gitlab/redis/wrapper.rb
@@ -8,9 +8,6 @@ module Gitlab
class Wrapper
DEFAULT_REDIS_URL = 'redis://localhost:6379'.freeze
REDIS_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_CONFIG_FILE'.freeze
- if defined?(::Rails) && ::Rails.root.present?
- DEFAULT_REDIS_CONFIG_FILE_NAME = ::Rails.root.join('config', 'resque.yml').freeze
- end
class << self
delegate :params, :url, to: :new
@@ -49,13 +46,21 @@ module Gitlab
DEFAULT_REDIS_URL
end
+ # Return the absolute path to a Rails configuration file
+ #
+ # We use this instead of `Rails.root` because for certain tasks
+ # utilizing these classes, `Rails` might not be available.
+ def config_file_path(filename)
+ File.expand_path("../../../config/#{filename}", __dir__)
+ end
+
def config_file_name
# if ENV set for wrapper class, use it even if it points to a file does not exist
file_name = ENV[REDIS_CONFIG_ENV_VAR_NAME]
return file_name unless file_name.nil?
# otherwise, if config files exists for wrapper class, use it
- file_name = File.expand_path(DEFAULT_REDIS_CONFIG_FILE_NAME, __dir__)
+ file_name = config_file_path('resque.yml')
return file_name if File.file?(file_name)
# nil will force use of DEFAULT_REDIS_URL when config file is absent
diff --git a/lib/gitlab/reference_counter.rb b/lib/gitlab/reference_counter.rb
new file mode 100644
index 00000000000..bb26f1b610a
--- /dev/null
+++ b/lib/gitlab/reference_counter.rb
@@ -0,0 +1,44 @@
+module Gitlab
+ class ReferenceCounter
+ REFERENCE_EXPIRE_TIME = 600
+
+ attr_reader :gl_repository, :key
+
+ def initialize(gl_repository)
+ @gl_repository = gl_repository
+ @key = "git-receive-pack-reference-counter:#{gl_repository}"
+ end
+
+ def value
+ Gitlab::Redis::SharedState.with { |redis| (redis.get(key) || 0).to_i }
+ end
+
+ def increase
+ redis_cmd do |redis|
+ redis.incr(key)
+ redis.expire(key, REFERENCE_EXPIRE_TIME)
+ end
+ end
+
+ def decrease
+ redis_cmd do |redis|
+ current_value = redis.decr(key)
+ if current_value < 0
+ Rails.logger.warn("Reference counter for #{gl_repository} decreased" \
+ " when its value was less than 1. Reseting the counter.")
+ redis.del(key)
+ end
+ end
+ end
+
+ private
+
+ def redis_cmd
+ Gitlab::Redis::SharedState.with { |redis| yield(redis) }
+ true
+ rescue => e
+ Rails.logger.warn("GitLab: An unexpected error occurred in writing to Redis: #{e}")
+ false
+ end
+ end
+end
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index f5b757ace77..bc836dcc08d 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -45,7 +45,7 @@ module Gitlab
end
def all
- REFERABLES.each { |referable| send(referable.to_s.pluralize) }
+ REFERABLES.each { |referable| send(referable.to_s.pluralize) } # rubocop:disable GitlabSecurity/PublicSend
@references.values.flatten
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 1adc5ec952a..58f6245579a 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -53,7 +53,8 @@ module Gitlab
end
def kubernetes_namespace_regex_message
- "can contain only letters, digits or '-', and cannot start or end with '-'"
+ "can contain only lowercase letters, digits, and '-'. " \
+ "Must start with a letter, and cannot end with '-'"
end
def environment_slug_regex
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
index 8a7cc690046..0f323a9e8b2 100644
--- a/lib/gitlab/saml/user.rb
+++ b/lib/gitlab/saml/user.rb
@@ -40,7 +40,7 @@ module Gitlab
end
def find_by_email
- if auth_hash.has_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
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 823f697f51c..f9ab9bd466f 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -1,3 +1,4 @@
+# :nocov:
module DeliverNever
def deliver_later
self
@@ -21,3 +22,4 @@ module Gitlab
end
end
end
+# :nocov:
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 2442c2ded3b..159d0e7952e 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -1,5 +1,7 @@
module Gitlab
module Sentry
+ extend Gitlab::CurrentSettings
+
def self.enabled?
Rails.env.production? && current_application_settings.sentry_enabled?
end
@@ -7,6 +9,8 @@ module Gitlab
def self.context(current_user = nil)
return unless self.enabled?
+ Raven.tags_context(locale: I18n.locale)
+
if current_user
Raven.user_context(
id: current_user.id,
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 0cb28732402..81ecdf43ef9 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -73,8 +73,10 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
def add_repository(storage, name)
- gitlab_shell_fast_execute([gitlab_shell_projects_path,
- 'add-project', storage, "#{name}.git"])
+ Gitlab::Git::Repository.create(storage, name, bare: true, symlink_hooks_to: gitlab_shell_hooks_path)
+ rescue => err
+ Rails.logger.error("Failed to add repository #{storage}/#{name}: #{err}")
+ false
end
# Import repository
@@ -96,33 +98,24 @@ module Gitlab
# Fetch remote for repository
#
- # name - project path with namespace
+ # repository - an instance of Git::Repository
# remote - remote name
# forced - should we use --force flag?
# no_tags - should we use --no-tags flag?
#
# Ex.
- # fetch_remote("gitlab/gitlab-ci", "upstream")
+ # fetch_remote(my_repo, "upstream")
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
- def fetch_remote(storage, name, remote, ssh_auth: nil, forced: false, no_tags: false)
- args = [gitlab_shell_projects_path, 'fetch-remote', storage, "#{name}.git", remote, "#{Gitlab.config.gitlab_shell.git_timeout}"]
- args << '--force' if forced
- args << '--no-tags' if no_tags
-
- vars = {}
-
- if ssh_auth&.ssh_import?
- if ssh_auth.ssh_key_auth? && ssh_auth.ssh_private_key.present?
- vars['GITLAB_SHELL_SSH_KEY'] = ssh_auth.ssh_private_key
- end
-
- if ssh_auth.ssh_known_hosts.present?
- vars['GITLAB_SHELL_KNOWN_HOSTS'] = ssh_auth.ssh_known_hosts
+ def fetch_remote(repository, remote, ssh_auth: nil, forced: false, no_tags: false)
+ gitaly_migrate(:fetch_remote) do |is_enabled|
+ if is_enabled
+ repository.gitaly_repository_client.fetch_remote(remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
+ else
+ storage_path = Gitlab.config.repositories.storages[repository.storage]["path"]
+ local_fetch_remote(storage_path, repository.relative_path, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
end
end
-
- gitlab_shell_fast_execute_raise_error(args, vars)
end
# Move repository
@@ -273,7 +266,11 @@ module Gitlab
protected
def gitlab_shell_path
- Gitlab.config.gitlab_shell.path
+ File.expand_path(Gitlab.config.gitlab_shell.path)
+ end
+
+ def gitlab_shell_hooks_path
+ File.expand_path(Gitlab.config.gitlab_shell.hooks_path)
end
def gitlab_shell_user_home
@@ -296,6 +293,26 @@ module Gitlab
private
+ def local_fetch_remote(storage, name, remote, ssh_auth: nil, forced: false, no_tags: false)
+ args = [gitlab_shell_projects_path, 'fetch-remote', storage, name, remote, "#{Gitlab.config.gitlab_shell.git_timeout}"]
+ args << '--force' if forced
+ args << '--no-tags' if no_tags
+
+ vars = {}
+
+ if ssh_auth&.ssh_import?
+ if ssh_auth.ssh_key_auth? && ssh_auth.ssh_private_key.present?
+ vars['GITLAB_SHELL_SSH_KEY'] = ssh_auth.ssh_private_key
+ end
+
+ if ssh_auth.ssh_known_hosts.present?
+ vars['GITLAB_SHELL_KNOWN_HOSTS'] = ssh_auth.ssh_known_hosts
+ end
+ end
+
+ gitlab_shell_fast_execute_raise_error(args, vars)
+ end
+
def gitlab_shell_fast_execute(cmd)
output, status = gitlab_shell_fast_execute_helper(cmd)
@@ -319,5 +336,13 @@ module Gitlab
# from wasting I/O by searching through GEM_PATH
Bundler.with_original_env { Popen.popen(cmd, nil, vars) }
end
+
+ def gitaly_migrate(method, &block)
+ Gitlab::GitalyClient.migrate(method, &block)
+ rescue GRPC::NotFound, GRPC::BadStatus => e
+ # Old Popen code returns [Error, output] to the caller, so we
+ # need to do the same here...
+ raise Error, e
+ end
end
end
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
index ca8d3271541..a0a2769cf9e 100644
--- a/lib/gitlab/sidekiq_status.rb
+++ b/lib/gitlab/sidekiq_status.rb
@@ -90,9 +90,14 @@ module Gitlab
#
# Returns an array of completed JIDs
def self.completed_jids(job_ids)
- Sidekiq.redis do |redis|
- job_ids.reject { |jid| redis.exists(key_for(jid)) }
+ statuses = job_status(job_ids)
+
+ completed = []
+ job_ids.zip(statuses).each do |job_id, status|
+ completed << job_id unless status
end
+
+ completed
end
def self.key_for(jid)
diff --git a/lib/gitlab/sidekiq_throttler.rb b/lib/gitlab/sidekiq_throttler.rb
index d4d39a888e7..5512afa45a8 100644
--- a/lib/gitlab/sidekiq_throttler.rb
+++ b/lib/gitlab/sidekiq_throttler.rb
@@ -3,6 +3,8 @@ module Gitlab
class << self
def execute!
if Gitlab::CurrentSettings.sidekiq_throttling_enabled?
+ require 'sidekiq-limit_fetch'
+
Gitlab::CurrentSettings.current_application_settings.sidekiq_throttling_queues.each do |queue|
Sidekiq::Queue[queue].limit = queue_limit
end
diff --git a/lib/gitlab/slash_commands/presenters/help.rb b/lib/gitlab/slash_commands/presenters/help.rb
index ea611a4d629..ab855319077 100644
--- a/lib/gitlab/slash_commands/presenters/help.rb
+++ b/lib/gitlab/slash_commands/presenters/help.rb
@@ -14,7 +14,7 @@ module Gitlab
if text.start_with?('help')
header_with_list("Available commands", full_commands(trigger))
else
- header_with_list("Unknown command, these commands are available", full_commands(trigger))
+ header_with_list("Unknown command, these commands are available", full_commands(trigger))
end
end
diff --git a/lib/gitlab/sql/pattern.rb b/lib/gitlab/sql/pattern.rb
new file mode 100644
index 00000000000..7c2d1d8f887
--- /dev/null
+++ b/lib/gitlab/sql/pattern.rb
@@ -0,0 +1,46 @@
+module Gitlab
+ module SQL
+ module Pattern
+ extend ActiveSupport::Concern
+
+ MIN_CHARS_FOR_PARTIAL_MATCHING = 3
+ REGEX_QUOTED_WORD = /(?<=^| )"[^"]+"(?= |$)/
+
+ class_methods do
+ def to_pattern(query)
+ if partial_matching?(query)
+ "%#{sanitize_sql_like(query)}%"
+ else
+ sanitize_sql_like(query)
+ end
+ end
+
+ def partial_matching?(query)
+ query.length >= MIN_CHARS_FOR_PARTIAL_MATCHING
+ end
+
+ def to_fuzzy_arel(column, query)
+ words = select_fuzzy_words(query)
+
+ matches = words.map { |word| arel_table[column].matches(to_pattern(word)) }
+
+ matches.reduce { |result, match| result.and(match) }
+ end
+
+ def select_fuzzy_words(query)
+ quoted_words = query.scan(REGEX_QUOTED_WORD)
+
+ query = quoted_words.reduce(query) { |q, quoted_word| q.sub(quoted_word, '') }
+
+ words = query.split(/\s+/)
+
+ quoted_words.map! { |quoted_word| quoted_word[1..-2] }
+
+ words.concat(quoted_words)
+
+ words.select { |word| partial_matching?(word) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ssh_public_key.rb b/lib/gitlab/ssh_public_key.rb
new file mode 100644
index 00000000000..89ca1298120
--- /dev/null
+++ b/lib/gitlab/ssh_public_key.rb
@@ -0,0 +1,71 @@
+module Gitlab
+ class SSHPublicKey
+ Technology = Struct.new(:name, :key_class, :supported_sizes)
+
+ Technologies = [
+ Technology.new(:rsa, OpenSSL::PKey::RSA, [1024, 2048, 3072, 4096]),
+ Technology.new(:dsa, OpenSSL::PKey::DSA, [1024, 2048, 3072]),
+ Technology.new(:ecdsa, OpenSSL::PKey::EC, [256, 384, 521]),
+ Technology.new(:ed25519, Net::SSH::Authentication::ED25519::PubKey, [256])
+ ].freeze
+
+ def self.technology(name)
+ Technologies.find { |tech| tech.name.to_s == name.to_s }
+ end
+
+ def self.technology_for_key(key)
+ Technologies.find { |tech| key.is_a?(tech.key_class) }
+ end
+
+ def self.supported_sizes(name)
+ technology(name)&.supported_sizes
+ end
+
+ attr_reader :key_text, :key
+
+ # Unqualified MD5 fingerprint for compatibility
+ delegate :fingerprint, to: :key, allow_nil: true
+
+ def initialize(key_text)
+ @key_text = key_text
+
+ @key =
+ begin
+ Net::SSH::KeyFactory.load_data_public_key(key_text)
+ rescue StandardError, NotImplementedError
+ end
+ end
+
+ def valid?
+ key.present?
+ end
+
+ def type
+ technology.name if valid?
+ end
+
+ def bits
+ return unless valid?
+
+ case type
+ when :rsa
+ key.n.num_bits
+ when :dsa
+ key.p.num_bits
+ when :ecdsa
+ key.group.order.num_bits
+ when :ed25519
+ 256
+ else
+ raise "Unsupported key type: #{type}"
+ end
+ end
+
+ private
+
+ def technology
+ @technology ||=
+ self.class.technology_for_key(key) || raise("Unsupported key type: #{key.class}")
+ end
+ end
+end
diff --git a/lib/gitlab/string_range_marker.rb b/lib/gitlab/string_range_marker.rb
index 94fba0a221a..11aeec1ebfa 100644
--- a/lib/gitlab/string_range_marker.rb
+++ b/lib/gitlab/string_range_marker.rb
@@ -1,21 +1,31 @@
module Gitlab
class StringRangeMarker
- attr_accessor :raw_line, :rich_line
-
- def initialize(raw_line, rich_line = raw_line)
- @raw_line = raw_line
- @rich_line = ERB::Util.html_escape(rich_line)
+ attr_accessor :raw_line, :rich_line, :html_escaped
+
+ def initialize(raw_line, rich_line = nil)
+ @raw_line = raw_line.dup
+ if rich_line.nil?
+ @rich_line = raw_line.dup
+ @html_escaped = false
+ else
+ @rich_line = ERB::Util.html_escape(rich_line)
+ @html_escaped = true
+ end
end
def mark(marker_ranges)
return rich_line unless marker_ranges
- rich_marker_ranges = []
- marker_ranges.each do |range|
- # Map the inline-diff range based on the raw line to character positions in the rich line
- rich_positions = position_mapping[range].flatten
- # Turn the array of character positions into ranges
- rich_marker_ranges.concat(collapse_ranges(rich_positions))
+ if html_escaped
+ rich_marker_ranges = []
+ marker_ranges.each do |range|
+ # Map the inline-diff range based on the raw line to character positions in the rich line
+ rich_positions = position_mapping[range].flatten
+ # Turn the array of character positions into ranges
+ rich_marker_ranges.concat(collapse_ranges(rich_positions))
+ end
+ else
+ rich_marker_ranges = marker_ranges
end
offset = 0
@@ -31,7 +41,7 @@ module Gitlab
offset += text.length - original_text.length
end
- rich_line.html_safe
+ @html_escaped ? rich_line.html_safe : rich_line
end
private
diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb
index 7ebec8e2cff..7393574ac13 100644
--- a/lib/gitlab/template/base_template.rb
+++ b/lib/gitlab/template/base_template.rb
@@ -18,6 +18,10 @@ module Gitlab
{ name: name, content: content }
end
+ def <=>(other)
+ name <=> other.name
+ end
+
class << self
def all(project = nil)
if categories.any?
@@ -58,7 +62,7 @@ module Gitlab
directory = category_directory(category)
files = finder(project).list_files_for(directory)
- files.map { |f| new(f, project) }
+ files.map { |f| new(f, project) }.sort
end
def category_directory(category)
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
new file mode 100644
index 00000000000..d43eff5ba4a
--- /dev/null
+++ b/lib/gitlab/themes.rb
@@ -0,0 +1,84 @@
+module Gitlab
+ # Module containing GitLab's application theme definitions and helper methods
+ # for accessing them.
+ module Themes
+ extend self
+
+ # Theme ID used when no `default_theme` configuration setting is provided.
+ APPLICATION_DEFAULT = 1
+
+ # Struct class representing a single Theme
+ Theme = Struct.new(:id, :name, :css_class)
+
+ # All available Themes
+ THEMES = [
+ Theme.new(1, 'Indigo', 'ui_indigo'),
+ Theme.new(2, 'Dark', 'ui_dark'),
+ Theme.new(3, 'Light', 'ui_light'),
+ Theme.new(4, 'Blue', 'ui_blue'),
+ Theme.new(5, 'Green', 'ui_green')
+ ].freeze
+
+ # Convenience method to get a space-separated String of all the theme
+ # classes that might be applied to the `body` element
+ #
+ # Returns a String
+ def body_classes
+ THEMES.collect(&:css_class).uniq.join(' ')
+ end
+
+ # Get a Theme by its ID
+ #
+ # If the ID is invalid, returns the default Theme.
+ #
+ # id - Integer ID
+ #
+ # Returns a Theme
+ def by_id(id)
+ THEMES.detect { |t| t.id == id } || default
+ end
+
+ # Returns the number of defined Themes
+ def count
+ THEMES.size
+ end
+
+ # Get the default Theme
+ #
+ # Returns a Theme
+ def default
+ by_id(default_id)
+ end
+
+ # Iterate through each Theme
+ #
+ # Yields the Theme object
+ def each(&block)
+ THEMES.each(&block)
+ end
+
+ # Get the Theme for the specified user, or the default
+ #
+ # user - User record
+ #
+ # Returns a Theme
+ def for_user(user)
+ if user
+ by_id(user.theme_id)
+ else
+ default
+ end
+ end
+
+ private
+
+ def default_id
+ @default_id ||= begin
+ id = Gitlab.config.gitlab.default_theme.to_i
+ theme_ids = THEMES.map(&:id)
+
+ theme_ids.include?(id) ? id : APPLICATION_DEFAULT
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index 7e14a566696..fee1a127fd7 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -19,6 +19,8 @@ module Gitlab
return false if internal?(uri)
return true if blocked_port?(uri.port)
+ return true if blocked_user_or_hostname?(uri.user)
+ return true if blocked_user_or_hostname?(uri.hostname)
server_ips = Resolv.getaddresses(uri.hostname)
return true if (blocked_ips & server_ips).any?
@@ -37,6 +39,12 @@ module Gitlab
port < 1024 && !VALID_PORTS.include?(port)
end
+ def blocked_user_or_hostname?(value)
+ return false if value.blank?
+
+ value !~ /\A\p{Alnum}/
+ end
+
def internal?(uri)
internal_web?(uri) || internal_shell?(uri)
end
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index c81dc7e30d0..703adae12cb 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -9,7 +9,7 @@ module Gitlab
end
def self.valid?(url)
- return false unless url
+ return false unless url.present?
Addressable::URI.parse(url.strip)
@@ -19,7 +19,12 @@ module Gitlab
end
def initialize(url, credentials: nil)
- @url = Addressable::URI.parse(url.strip)
+ @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
end
@@ -29,13 +34,13 @@ module Gitlab
def masked_url
url = @url.dup
- url.password = "*****" unless url.password.nil?
- url.user = "*****" unless url.user.nil?
+ url.password = "*****" if url.password.present?
+ url.user = "*****" if url.user.present?
url.to_s
end
def credentials
- @credentials ||= { user: @url.user, password: @url.password }
+ @credentials ||= { user: @url.user.presence, password: @url.password.presence }
end
def full_url
@@ -47,8 +52,10 @@ module Gitlab
def generate_full_url
return @url unless valid_credentials?
@full_url = @url.dup
- @full_url.user = credentials[:user]
+
@full_url.password = credentials[:password]
+ @full_url.user = credentials[:user]
+
@full_url
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 748e0a29184..3cf26625108 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -1,8 +1,8 @@
module Gitlab
class UsageData
- include Gitlab::CurrentSettings
-
class << self
+ include Gitlab::CurrentSettings
+
def data(force_refresh: false)
Rails.cache.fetch('usage_data', force: force_refresh, expires_in: 2.weeks) { uncached_data }
end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index fa182c4deda..abb3d3a02c3 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -14,6 +14,19 @@ module Gitlab
str.force_encoding(Encoding::UTF_8)
end
+ # A slugified version of the string, suitable for inclusion in URLs and
+ # domain names. Rules:
+ #
+ # * Lowercased
+ # * Anything not matching [a-z0-9-] is replaced with a -
+ # * Maximum length is 63 bytes
+ # * First/Last Character is not a hyphen
+ def slugify(str)
+ return str.downcase
+ .gsub(/[^a-z0-9]/, '-')[0..62]
+ .gsub(/(\A-+|-+\z)/, '')
+ end
+
def to_boolean(value)
return value if [true, false].include?(value)
return true if value =~ /^(true|t|yes|y|1|on)$/i
@@ -29,5 +42,9 @@ module Gitlab
'No'
end
end
+
+ def random_string
+ Random.rand(Float::MAX.to_i).to_s(36)
+ end
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 3f25e463412..7a94af2f8f1 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -35,17 +35,13 @@ module Gitlab
when 'git_receive_pack'
Gitlab::GitalyClient.feature_enabled?(:post_receive_pack)
when 'git_upload_pack'
- Gitlab::GitalyClient.feature_enabled?(
- :post_upload_pack,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT
- )
+ true
when 'info_refs'
true
else
raise "Unsupported action: #{action}"
end
if feature_enabled
- params[:GitalyAddress] = server[:address] # This field will be deprecated
params[:GitalyServer] = server
end
@@ -125,10 +121,10 @@ module Gitlab
]
end
- def send_artifacts_entry(build, entry)
+ def send_artifacts_entry(build, path)
params = {
'Archive' => build.artifacts_file.path,
- 'Entry' => Base64.encode64(entry.path)
+ 'Entry' => Base64.encode64(path.to_s)
}
[
diff --git a/lib/rspec_flaky/example.rb b/lib/rspec_flaky/example.rb
new file mode 100644
index 00000000000..b6e790cbbab
--- /dev/null
+++ b/lib/rspec_flaky/example.rb
@@ -0,0 +1,46 @@
+module RspecFlaky
+ # This is a wrapper class for RSpec::Core::Example
+ class Example
+ delegate :status, :exception, to: :execution_result
+
+ def initialize(rspec_example)
+ @rspec_example = rspec_example.try(:example) || rspec_example
+ end
+
+ def uid
+ @uid ||= Digest::MD5.hexdigest("#{description}-#{file}")
+ end
+
+ def example_id
+ rspec_example.id
+ end
+
+ def file
+ metadata[:file_path]
+ end
+
+ def line
+ metadata[:line_number]
+ end
+
+ def description
+ metadata[:full_description]
+ end
+
+ def attempts
+ rspec_example.try(:attempts) || 1
+ end
+
+ private
+
+ attr_reader :rspec_example
+
+ def metadata
+ rspec_example.metadata
+ end
+
+ def execution_result
+ rspec_example.execution_result
+ end
+ end
+end
diff --git a/lib/rspec_flaky/flaky_example.rb b/lib/rspec_flaky/flaky_example.rb
new file mode 100644
index 00000000000..f81fb90e870
--- /dev/null
+++ b/lib/rspec_flaky/flaky_example.rb
@@ -0,0 +1,39 @@
+module RspecFlaky
+ # This represents a flaky RSpec example and is mainly meant to be saved in a JSON file
+ class FlakyExample < OpenStruct
+ def initialize(example)
+ if example.respond_to?(:example_id)
+ super(
+ example_id: example.example_id,
+ file: example.file,
+ line: example.line,
+ description: example.description,
+ last_attempts_count: example.attempts,
+ flaky_reports: 1)
+ else
+ super
+ end
+ end
+
+ def first_flaky_at
+ self[:first_flaky_at] || Time.now
+ end
+
+ def last_flaky_at
+ Time.now
+ end
+
+ def last_flaky_job
+ return unless ENV['CI_PROJECT_URL'] && ENV['CI_JOB_ID']
+
+ "#{ENV['CI_PROJECT_URL']}/-/jobs/#{ENV['CI_JOB_ID']}"
+ end
+
+ def to_h
+ super.merge(
+ first_flaky_at: first_flaky_at,
+ last_flaky_at: last_flaky_at,
+ last_flaky_job: last_flaky_job)
+ end
+ end
+end
diff --git a/lib/rspec_flaky/listener.rb b/lib/rspec_flaky/listener.rb
new file mode 100644
index 00000000000..ec2fbd9e36c
--- /dev/null
+++ b/lib/rspec_flaky/listener.rb
@@ -0,0 +1,75 @@
+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
+ end
+
+ def example_passed(notification)
+ current_example = RspecFlaky::Example.new(notification.example)
+
+ 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
+ end
+
+ def dump_summary(_)
+ write_report_file(all_flaky_examples, all_flaky_examples_report_path)
+
+ if new_flaky_examples.any?
+ Rails.logger.warn "\nNew flaky examples detected:\n"
+ Rails.logger.warn JSON.pretty_generate(to_report(new_flaky_examples))
+
+ write_report_file(new_flaky_examples, new_flaky_examples_report_path)
+ end
+ end
+
+ def to_report(examples)
+ Hash[examples.map { |k, ex| [k, ex.to_h] }]
+ end
+
+ private
+
+ def init_all_flaky_examples
+ return {} unless File.exist?(all_flaky_examples_report_path)
+
+ all_flaky_examples = JSON.parse(File.read(all_flaky_examples_report_path))
+
+ Hash[(all_flaky_examples || {}).map { |k, ex| [k, FlakyExample.new(ex)] }]
+ end
+
+ def write_report_file(examples, file_path)
+ return unless ENV['FLAKY_RSPEC_GENERATE_REPORT'] == 'true'
+
+ 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")
+ end
+ end
+end
diff --git a/lib/static_model.rb b/lib/static_model.rb
index 185921d8fbe..60e2dd82e4e 100644
--- a/lib/static_model.rb
+++ b/lib/static_model.rb
@@ -18,7 +18,7 @@ module StaticModel
#
# Pass it along if we respond to it.
def [](key)
- send(key) if respond_to?(key)
+ send(key) if respond_to?(key) # rubocop:disable GitlabSecurity/PublicSend
end
def to_param
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index f25e66d54c8..54f51d9d633 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -25,6 +25,39 @@ map $http_upgrade $connection_upgrade_gitlab {
'' close;
}
+## NGINX 'combined' log format with filtered query strings
+log_format gitlab_access $remote_addr - $remote_user [$time_local] "$request_method $gitlab_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_filtered_http_referer" "$http_user_agent";
+
+## Remove private_token from the request URI
+# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&rss_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&...
+map $request_uri $gitlab_temp_request_uri_1 {
+ default $request_uri;
+ ~(?i)^(?<start>.*)(?<temp>[\?&]private[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
+}
+
+## Remove authenticity_token from the request URI
+# In: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&...
+map $gitlab_temp_request_uri_1 $gitlab_temp_request_uri_2 {
+ default $gitlab_temp_request_uri_1;
+ ~(?i)^(?<start>.*)(?<temp>[\?&]authenticity[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
+}
+
+## Remove rss_token from the request URI
+# In: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=[FILTERED]&...
+map $gitlab_temp_request_uri_2 $gitlab_filtered_request_uri {
+ default $gitlab_temp_request_uri_2;
+ ~(?i)^(?<start>.*)(?<temp>[\?&]rss[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
+}
+
+## A version of the referer without the query string
+map $http_referer $gitlab_filtered_http_referer {
+ default $http_referer;
+ ~^(?<temp>.*)\? $temp;
+}
+
## Normal HTTP host
server {
## Either remove "default_server" from the listen line below,
@@ -46,7 +79,7 @@ server {
# set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
## Individual nginx logs for this GitLab vhost
- access_log /var/log/nginx/gitlab_access.log;
+ access_log /var/log/nginx/gitlab_access.log gitlab_access;
error_log /var/log/nginx/gitlab_error.log;
location / {
diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages
index d9746c5c1aa..875c8bcbf3c 100644
--- a/lib/support/nginx/gitlab-pages
+++ b/lib/support/nginx/gitlab-pages
@@ -18,8 +18,11 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_cache off;
+
# The same address as passed to GitLab Pages: `-listen-proxy`
- proxy_pass http://localhost:8090/;
+ proxy_pass http://localhost:8090/;
}
# Define custom error pages
diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl
index a1ccf266835..62ed482e2bf 100644
--- a/lib/support/nginx/gitlab-pages-ssl
+++ b/lib/support/nginx/gitlab-pages-ssl
@@ -67,8 +67,11 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_cache off;
+
# The same address as passed to GitLab Pages: `-listen-proxy`
- proxy_pass http://localhost:8090/;
+ proxy_pass http://localhost:8090/;
}
# Define custom error pages
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 2b40da18bab..ed8131ef24f 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -29,6 +29,41 @@ map $http_upgrade $connection_upgrade_gitlab_ssl {
'' close;
}
+
+## NGINX 'combined' log format with filtered query strings
+log_format gitlab_ssl_access $remote_addr - $remote_user [$time_local] "$request_method $gitlab_ssl_filtered_request_uri $server_protocol" $status $body_bytes_sent "$gitlab_ssl_filtered_http_referer" "$http_user_agent";
+
+## Remove private_token from the request URI
+# In: /foo?private_token=unfiltered&authenticity_token=unfiltered&rss_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&...
+map $request_uri $gitlab_ssl_temp_request_uri_1 {
+ default $request_uri;
+ ~(?i)^(?<start>.*)(?<temp>[\?&]private[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
+}
+
+## Remove authenticity_token from the request URI
+# In: /foo?private_token=[FILTERED]&authenticity_token=unfiltered&rss_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&...
+map $gitlab_ssl_temp_request_uri_1 $gitlab_ssl_temp_request_uri_2 {
+ default $gitlab_ssl_temp_request_uri_1;
+ ~(?i)^(?<start>.*)(?<temp>[\?&]authenticity[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
+}
+
+## Remove rss_token from the request URI
+# In: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=unfiltered&...
+# Out: /foo?private_token=[FILTERED]&authenticity_token=[FILTERED]&rss_token=[FILTERED]&...
+map $gitlab_ssl_temp_request_uri_2 $gitlab_ssl_filtered_request_uri {
+ default $gitlab_ssl_temp_request_uri_2;
+ ~(?i)^(?<start>.*)(?<temp>[\?&]rss[\-_]token)=[^&]*(?<rest>.*)$ "$start$temp=[FILTERED]$rest";
+}
+
+## A version of the referer without the query string
+map $http_referer $gitlab_ssl_filtered_http_referer {
+ default $http_referer;
+ ~^(?<temp>.*)\? $temp;
+}
+
+
## Redirects all HTTP traffic to the HTTPS host
server {
## Either remove "default_server" from the listen line below,
@@ -40,7 +75,7 @@ server {
server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com
server_tokens off; ## Don't show the nginx version number, a security best practice
return 301 https://$http_host$request_uri;
- access_log /var/log/nginx/gitlab_access.log;
+ access_log /var/log/nginx/gitlab_access.log gitlab_ssl_access;
error_log /var/log/nginx/gitlab_error.log;
}
@@ -93,7 +128,7 @@ server {
# set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
## Individual nginx logs for this GitLab vhost
- access_log /var/log/nginx/gitlab_access.log;
+ access_log /var/log/nginx/gitlab_access.log gitlab_ssl_access;
error_log /var/log/nginx/gitlab_error.log;
location / {
diff --git a/lib/system_check/app/git_config_check.rb b/lib/system_check/app/git_config_check.rb
index 198867f7ac6..d08a81639e3 100644
--- a/lib/system_check/app/git_config_check.rb
+++ b/lib/system_check/app/git_config_check.rb
@@ -20,7 +20,7 @@ module SystemCheck
# Returns true if all subcommands were successful (according to their exit code)
# Returns false if any or all subcommands failed.
def repair!
- return false unless is_gitlab_user?
+ return false unless gitlab_user?
command_success = OPTIONS.map do |name, value|
system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value}))
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
new file mode 100644
index 00000000000..7b486d78cf0
--- /dev/null
+++ b/lib/system_check/app/git_user_default_ssh_config_check.rb
@@ -0,0 +1,69 @@
+module SystemCheck
+ module App
+ class GitUserDefaultSSHConfigCheck < SystemCheck::BaseCheck
+ # These files are allowed in the .ssh directory. The `config` file is not
+ # whitelisted as it may change the SSH client's behaviour dramatically.
+ WHITELIST = %w[
+ authorized_keys
+ authorized_keys2
+ known_hosts
+ ].freeze
+
+ set_name 'Git user has default SSH configuration?'
+ set_skip_reason 'skipped (git user is not present or configured)'
+
+ def skip?
+ !home_dir || !File.directory?(home_dir)
+ end
+
+ def check?
+ forbidden_files.empty?
+ end
+
+ def show_error
+ backup_dir = "~/gitlab-check-backup-#{Time.now.to_i}"
+
+ instructions = forbidden_files.map do |filename|
+ "sudo mv #{Shellwords.escape(filename)} #{backup_dir}"
+ end
+
+ try_fixing_it("mkdir #{backup_dir}", *instructions)
+ for_more_information('doc/ssh/README.md in section "SSH on the GitLab server"')
+ fix_and_rerun
+ end
+
+ private
+
+ def git_user
+ Gitlab.config.gitlab.user
+ end
+
+ def home_dir
+ return @home_dir if defined?(@home_dir)
+
+ @home_dir =
+ begin
+ File.expand_path("~#{git_user}")
+ rescue ArgumentError
+ nil
+ end
+ end
+
+ def ssh_dir
+ return nil unless home_dir
+
+ File.join(home_dir, '.ssh')
+ end
+
+ def forbidden_files
+ @forbidden_files ||=
+ begin
+ present = Dir[File.join(ssh_dir, '*')]
+ whitelisted = WHITELIST.map { |basename| File.join(ssh_dir, basename) }
+
+ present - whitelisted
+ end
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/init_script_up_to_date_check.rb b/lib/system_check/app/init_script_up_to_date_check.rb
index 015c7ed1731..53a47eb0f42 100644
--- a/lib/system_check/app/init_script_up_to_date_check.rb
+++ b/lib/system_check/app/init_script_up_to_date_check.rb
@@ -7,26 +7,22 @@ module SystemCheck
set_skip_reason 'skipped (omnibus-gitlab has no init script)'
def skip?
- omnibus_gitlab?
- end
+ return true if omnibus_gitlab?
- def multi_check
- recipe_path = Rails.root.join('lib/support/init.d/', 'gitlab')
+ unless init_file_exists?
+ self.skip_reason = "can't check because of previous errors"
- unless File.exist?(SCRIPT_PATH)
- $stdout.puts "can't check because of previous errors".color(:magenta)
- return
+ true
end
+ end
+
+ def check?
+ recipe_path = Rails.root.join('lib/support/init.d/', 'gitlab')
recipe_content = File.read(recipe_path)
script_content = File.read(SCRIPT_PATH)
- if recipe_content == script_content
- $stdout.puts 'yes'.color(:green)
- else
- $stdout.puts 'no'.color(:red)
- show_error
- end
+ recipe_content == script_content
end
def show_error
@@ -38,6 +34,12 @@ module SystemCheck
)
fix_and_rerun
end
+
+ private
+
+ def init_file_exists?
+ File.exist?(SCRIPT_PATH)
+ end
end
end
end
diff --git a/lib/system_check/base_check.rb b/lib/system_check/base_check.rb
index 5dcb3f0886b..0f5742dd67f 100644
--- a/lib/system_check/base_check.rb
+++ b/lib/system_check/base_check.rb
@@ -62,6 +62,25 @@ module SystemCheck
call_or_return(@skip_reason) || 'skipped'
end
+ # Define a reason why we skipped the SystemCheck (during runtime)
+ #
+ # This is used when you need dynamic evaluation like when you have
+ # multiple reasons why a check can fail
+ #
+ # @param [String] reason to be displayed
+ def skip_reason=(reason)
+ @skip_reason = reason
+ end
+
+ # Skip reason defined during runtime
+ #
+ # This value have precedence over the one defined in the subclass
+ #
+ # @return [String] the reason
+ def skip_reason
+ @skip_reason
+ end
+
# Does the check support automatically repair routine?
#
# @return [Boolean] whether check implemented `#repair!` method or not
@@ -73,7 +92,7 @@ module SystemCheck
self.class.instance_methods(false).include?(:skip?)
end
- def is_multi_check?
+ def multi_check?
self.class.instance_methods(false).include?(:multi_check)
end
diff --git a/lib/system_check/incoming_email/foreman_configured_check.rb b/lib/system_check/incoming_email/foreman_configured_check.rb
new file mode 100644
index 00000000000..1db7bf2b782
--- /dev/null
+++ b/lib/system_check/incoming_email/foreman_configured_check.rb
@@ -0,0 +1,23 @@
+module SystemCheck
+ module IncomingEmail
+ class ForemanConfiguredCheck < SystemCheck::BaseCheck
+ set_name 'Foreman configured correctly?'
+
+ def check?
+ path = Rails.root.join('Procfile')
+
+ File.exist?(path) && File.read(path) =~ /^mail_room:/
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Enable mail_room in your Procfile.'
+ )
+ for_more_information(
+ 'doc/administration/reply_by_email.md'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/incoming_email/imap_authentication_check.rb b/lib/system_check/incoming_email/imap_authentication_check.rb
new file mode 100644
index 00000000000..dee108d987b
--- /dev/null
+++ b/lib/system_check/incoming_email/imap_authentication_check.rb
@@ -0,0 +1,45 @@
+module SystemCheck
+ module IncomingEmail
+ class ImapAuthenticationCheck < SystemCheck::BaseCheck
+ 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
+ end
+
+ connected
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Check that the information in config/gitlab.yml is correct'
+ )
+ for_more_information(
+ 'doc/administration/reply_by_email.md'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def mailbox_config
+ return @config if @config
+
+ config_path = Rails.root.join('config', 'mail_room.yml').to_s
+ erb = ERB.new(File.read(config_path))
+ erb.filename = config_path
+ config_file = YAML.load(erb.result)
+
+ @config = config_file[:mailboxes]&.first
+ end
+ end
+ end
+end
diff --git a/lib/system_check/incoming_email/initd_configured_check.rb b/lib/system_check/incoming_email/initd_configured_check.rb
new file mode 100644
index 00000000000..ea23b8ef49c
--- /dev/null
+++ b/lib/system_check/incoming_email/initd_configured_check.rb
@@ -0,0 +1,32 @@
+module SystemCheck
+ module IncomingEmail
+ class InitdConfiguredCheck < SystemCheck::BaseCheck
+ set_name 'Init.d configured correctly?'
+
+ def skip?
+ omnibus_gitlab?
+ end
+
+ def check?
+ mail_room_configured?
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Enable mail_room in the init.d configuration.'
+ )
+ for_more_information(
+ 'doc/administration/reply_by_email.md'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def mail_room_configured?
+ path = '/etc/default/gitlab'
+ File.exist?(path) && File.read(path).include?('mail_room_enabled=true')
+ end
+ end
+ end
+end
diff --git a/lib/system_check/incoming_email/mail_room_running_check.rb b/lib/system_check/incoming_email/mail_room_running_check.rb
new file mode 100644
index 00000000000..c1807501829
--- /dev/null
+++ b/lib/system_check/incoming_email/mail_room_running_check.rb
@@ -0,0 +1,43 @@
+module SystemCheck
+ module IncomingEmail
+ class MailRoomRunningCheck < SystemCheck::BaseCheck
+ set_name 'MailRoom running?'
+
+ def skip?
+ return true if omnibus_gitlab?
+
+ unless mail_room_configured?
+ self.skip_reason = "can't check because of previous errors"
+ true
+ end
+ end
+
+ def check?
+ mail_room_running?
+ end
+
+ def show_error
+ try_fixing_it(
+ sudo_gitlab('RAILS_ENV=production bin/mail_room start')
+ )
+ for_more_information(
+ see_installation_guide_section('Install Init Script'),
+ 'see log/mail_room.log for possible errors'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def mail_room_configured?
+ path = '/etc/default/gitlab'
+ File.exist?(path) && File.read(path).include?('mail_room_enabled=true')
+ end
+
+ def mail_room_running?
+ ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww))
+ ps_ux.include?("mail_room")
+ end
+ end
+ end
+end
diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb
index e5986612908..00221f77cf4 100644
--- a/lib/system_check/simple_executor.rb
+++ b/lib/system_check/simple_executor.rb
@@ -23,7 +23,7 @@ module SystemCheck
#
# @param [BaseCheck] check class
def <<(check)
- raise ArgumentError unless check < BaseCheck
+ raise ArgumentError unless check.is_a?(Class) && check < BaseCheck
@checks << check
end
@@ -48,12 +48,12 @@ module SystemCheck
# When implements skip method, we run it first, and if true, skip the check
if check.can_skip? && check.skip?
- $stdout.puts check_klass.skip_reason.color(:magenta)
+ $stdout.puts check.skip_reason.try(:color, :magenta) || check_klass.skip_reason.color(:magenta)
return
end
# When implements a multi check, we don't control the output
- if check.is_multi_check?
+ if check.multi_check?
check.multi_check
return
end
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index b48e4dce445..35ba729c156 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -19,4 +19,46 @@ namespace :gettext do
Rake::Task['gettext:pack'].invoke
Rake::Task['gettext:po_to_json'].invoke
end
+
+ desc 'Lint all po files in `locale/'
+ task lint: :environment do
+ require 'simple_po_parser'
+
+ FastGettext.silence_errors
+ files = Dir.glob(Rails.root.join('locale/*/gitlab.po'))
+
+ linters = files.map do |file|
+ locale = File.basename(File.dirname(file))
+
+ Gitlab::I18n::PoLinter.new(file, locale)
+ end
+
+ pot_file = Rails.root.join('locale/gitlab.pot')
+ linters.unshift(Gitlab::I18n::PoLinter.new(pot_file))
+
+ failed_linters = linters.select { |linter| linter.errors.any? }
+
+ if failed_linters.empty?
+ puts 'All PO files are valid.'
+ else
+ failed_linters.each do |linter|
+ report_errors_for_file(linter.po_path, linter.errors)
+ end
+
+ raise "Not all PO-files are valid: #{failed_linters.map(&:po_path).to_sentence}"
+ end
+ end
+
+ def report_errors_for_file(file, errors_for_file)
+ puts "Errors in `#{file}`:"
+
+ errors_for_file.each do |message_id, errors|
+ puts " #{message_id}"
+ errors.each do |error|
+ spaces = ' ' * 4
+ error = error.lines.join("#{spaces}")
+ puts "#{spaces}#{error}"
+ end
+ end
+ end
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index dbb3b827b9a..654f638c454 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -33,6 +33,7 @@ namespace :gitlab do
SystemCheck::App::RedisVersionCheck,
SystemCheck::App::RubyVersionCheck,
SystemCheck::App::GitVersionCheck,
+ SystemCheck::App::GitUserDefaultSSHConfigCheck,
SystemCheck::App::ActiveUsersCheck
]
@@ -41,8 +42,6 @@ namespace :gitlab do
end
namespace :gitlab_shell do
- include SystemCheck::Helpers
-
desc "GitLab | Check the configuration of GitLab Shell"
task check: :environment do
warn_user_is_not_gitlab
@@ -249,8 +248,6 @@ namespace :gitlab do
end
namespace :sidekiq do
- include SystemCheck::Helpers
-
desc "GitLab | Check the configuration of Sidekiq"
task check: :environment do
warn_user_is_not_gitlab
@@ -309,143 +306,30 @@ namespace :gitlab do
end
namespace :incoming_email do
- include SystemCheck::Helpers
-
desc "GitLab | Check the configuration of Reply by email"
task check: :environment do
warn_user_is_not_gitlab
- start_checking "Reply by email"
if Gitlab.config.incoming_email.enabled
- check_imap_authentication
+ checks = [
+ SystemCheck::IncomingEmail::ImapAuthenticationCheck
+ ]
if Rails.env.production?
- check_initd_configured_correctly
- check_mail_room_running
+ checks << SystemCheck::IncomingEmail::InitdConfiguredCheck
+ checks << SystemCheck::IncomingEmail::MailRoomRunningCheck
else
- check_foreman_configured_correctly
+ checks << SystemCheck::IncomingEmail::ForemanConfiguredCheck
end
- else
- puts 'Reply by email is disabled in config/gitlab.yml'
- end
-
- finished_checking "Reply by email"
- end
-
- # Checks
- ########################
-
- def check_initd_configured_correctly
- return if omnibus_gitlab?
-
- print "Init.d configured correctly? ... "
- path = "/etc/default/gitlab"
-
- if File.exist?(path) && File.read(path).include?("mail_room_enabled=true")
- puts "yes".color(:green)
+ SystemCheck.run('Reply by email', checks)
else
- puts "no".color(:red)
- try_fixing_it(
- "Enable mail_room in the init.d configuration."
- )
- for_more_information(
- "doc/administration/reply_by_email.md"
- )
- fix_and_rerun
- end
- end
-
- def check_foreman_configured_correctly
- print "Foreman configured correctly? ... "
-
- path = Rails.root.join("Procfile")
-
- if File.exist?(path) && File.read(path) =~ /^mail_room:/
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Enable mail_room in your Procfile."
- )
- for_more_information(
- "doc/administration/reply_by_email.md"
- )
- fix_and_rerun
- end
- end
-
- def check_mail_room_running
- return if omnibus_gitlab?
-
- print "MailRoom running? ... "
-
- path = "/etc/default/gitlab"
-
- unless File.exist?(path) && File.read(path).include?("mail_room_enabled=true")
- puts "can't check because of previous errors".color(:magenta)
- return
- end
-
- if mail_room_running?
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- sudo_gitlab("RAILS_ENV=production bin/mail_room start")
- )
- for_more_information(
- see_installation_guide_section("Install Init Script"),
- "see log/mail_room.log for possible errors"
- )
- fix_and_rerun
- end
- end
-
- def check_imap_authentication
- print "IMAP server credentials are correct? ... "
-
- config_path = Rails.root.join('config', 'mail_room.yml').to_s
- erb = ERB.new(File.read(config_path))
- erb.filename = config_path
- config_file = YAML.load(erb.result)
-
- config = config_file[:mailboxes].first
-
- if 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
- end
-
- if connected
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Check that the information in config/gitlab.yml is correct"
- )
- for_more_information(
- "doc/administration/reply_by_email.md"
- )
- fix_and_rerun
+ puts 'Reply by email is disabled in config/gitlab.yml'
end
end
-
- def mail_room_running?
- ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww))
- ps_ux.include?("mail_room")
- end
end
namespace :ldap do
- include SystemCheck::Helpers
-
task :check, [:limit] => :environment do |_, args|
# Only show up to 100 results because LDAP directories can be very big.
# This setting only affects the `rake gitlab:check` script.
@@ -501,8 +385,6 @@ namespace :gitlab do
end
namespace :repo do
- include SystemCheck::Helpers
-
desc "GitLab | Check the integrity of the repositories managed by GitLab"
task check: :environment do
Gitlab.config.repositories.storages.each do |name, repository_storage|
@@ -517,8 +399,6 @@ namespace :gitlab do
end
namespace :user do
- include SystemCheck::Helpers
-
desc "GitLab | Check the integrity of a specific user's repositories"
task :check_repos, [:username] => :environment do |t, args|
username = args[:username] || prompt("Check repository integrity for fsername? ".color(:blue))
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index f76bef5f4bf..8ae1b6a626a 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -111,7 +111,7 @@ namespace :gitlab do
next unless id > max_iid
project.deployments.find(id).create_ref
- rugged.references.delete(ref)
+ project.repository.delete_refs(ref)
end
end
end
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index aaf00bd703a..08677a98fc1 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -15,13 +15,17 @@ namespace :gitlab do
checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir)
_, status = Gitlab::Popen.popen(%w[which gmake])
- command = status.zero? ? 'gmake' : 'make'
+ command = status.zero? ? ['gmake'] : ['make']
+
+ if Rails.env.test?
+ command += %W[BUNDLE_PATH=#{Bundler.bundle_path}]
+ end
Dir.chdir(args.dir) do
create_gitaly_configuration
# In CI we run scripts/gitaly-test-build instead of this command
unless ENV['CI'].present?
- Bundler.with_original_env { run_command!(%w[/usr/bin/env -u RUBYOPT] + [command]) }
+ Bundler.with_original_env { run_command!(%w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] + command) }
end
end
end
diff --git a/lib/tasks/gitlab/helpers.rake b/lib/tasks/gitlab/helpers.rake
index dd2d5861481..b0a24790c4a 100644
--- a/lib/tasks/gitlab/helpers.rake
+++ b/lib/tasks/gitlab/helpers.rake
@@ -4,5 +4,5 @@ require 'tasks/gitlab/task_helpers'
StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
namespace :gitlab do
- include Gitlab::TaskHelpers
+ extend SystemCheck::Helpers
end
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index 48bd9139ce8..d227a0c8bdb 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -9,71 +9,16 @@ namespace :gitlab do
# * The project owner will set to the first administator of the system
# * Existing projects will be skipped
#
+ #
desc "GitLab | Import bare repositories from repositories -> storages into GitLab project instance"
task repos: :environment do
- Gitlab.config.repositories.storages.each_value do |repository_storage|
- git_base_path = repository_storage['path']
- repos_to_import = Dir.glob(git_base_path + '/**/*.git')
-
- repos_to_import.each do |repo_path|
- # strip repo base path
- repo_path[0..git_base_path.length] = ''
-
- path = repo_path.sub(/\.git$/, '')
- group_name, name = File.split(path)
- group_name = nil if group_name == '.'
-
- puts "Processing #{repo_path}".color(:yellow)
-
- if path.end_with?('.wiki')
- puts " * Skipping wiki repo"
- next
- end
-
- project = Project.find_by_full_path(path)
-
- if project
- puts " * #{project.name} (#{repo_path}) exists"
- else
- user = User.admins.reorder("id").first
-
- project_params = {
- name: name,
- path: name
- }
-
- # find group namespace
- if group_name
- group = Namespace.find_by(path: group_name)
- # create group namespace
- unless group
- group = Group.new(name: group_name)
- group.path = group_name
- group.owner = user
- if group.save
- puts " * Created Group #{group.name} (#{group.id})".color(:green)
- else
- puts " * Failed trying to create group #{group.name}".color(:red)
- end
- end
- # set project group
- project_params[:namespace_id] = group.id
- end
-
- project = Projects::CreateService.new(user, project_params).execute
+ if Project.current_application_settings.hashed_storage_enabled
+ puts 'Cannot import repositories when Hashed Storage is enabled'.color(:red)
- if project.persisted?
- puts " * Created #{project.name} (#{repo_path})".color(:green)
- ProjectCacheWorker.perform_async(project.id)
- else
- puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red)
- puts " Errors: #{project.errors.messages}".color(:red)
- end
- end
- end
+ exit 1
end
- puts "Done!".color(:green)
+ Gitlab::BareRepositoryImporter.execute
end
end
end
diff --git a/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake
index dd1825c8a9e..44074397c05 100644
--- a/lib/tasks/gitlab/import_export.rake
+++ b/lib/tasks/gitlab/import_export.rake
@@ -9,5 +9,16 @@ namespace :gitlab do
task data: :environment do
puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(SortKeys: true)
end
+
+ desc 'GitLab | Bumps the Import/Export version for test_project_export.tar.gz'
+ task bump_test_version: :environment do
+ Dir.mktmpdir do |tmp_dir|
+ system("tar -zxf spec/features/projects/import_export/test_project_export.tar.gz -C #{tmp_dir} > /dev/null")
+ File.write(File.join(tmp_dir, 'VERSION'), Gitlab::ImportExport.version, mode: 'w')
+ system("tar -zcvf spec/features/projects/import_export/test_project_export.tar.gz -C #{tmp_dir} . > /dev/null")
+ end
+
+ puts "Updated to #{Gitlab::ImportExport.version}"
+ end
end
end
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
index 28b2d86eed2..8a63f486fa3 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -5,6 +5,8 @@ module Gitlab
TaskAbortedByUserError = Class.new(StandardError)
module TaskHelpers
+ extend self
+
# Ask if the user wants to continue
#
# Returns "yes" the user chose to continue
@@ -102,7 +104,7 @@ module Gitlab
Gitlab.config.gitlab.user
end
- def is_gitlab_user?
+ def gitlab_user?
return @is_gitlab_user unless @is_gitlab_user.nil?
current_user = run_command(%w(whoami)).chomp
@@ -112,7 +114,7 @@ module Gitlab
def warn_user_is_not_gitlab
return if @warned_user_not_gitlab
- unless is_gitlab_user?
+ unless gitlab_user?
current_user = run_command(%w(whoami)).chomp
puts " Warning ".color(:black).background(:yellow)
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index a7e30423c7a..f44abc2b81b 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -21,13 +21,18 @@ namespace :gitlab do
params = {
import_url: template.clone_url,
namespace_id: admin.namespace.id,
- path: template.title,
+ path: template.name,
skip_wiki: true
}
- puts "Creating project for #{template.name}"
+ puts "Creating project for #{template.title}"
project = Projects::CreateService.new(admin, params).execute
+ unless project.persisted?
+ puts project.errors.messages
+ exit(1)
+ end
+
loop do
if project.finished?
puts "Import finished for #{template.name}"
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index 96b8f59242c..4d485108cf6 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -72,23 +72,7 @@ class GithubImport
return @current_user.namespace if names == @current_user.namespace_path
return @current_user.namespace unless @current_user.can_create_group?
- full_path_namespace = Namespace.find_by_full_path(names)
-
- return full_path_namespace if full_path_namespace
-
- names.split('/').inject(nil) do |parent, name|
- begin
- namespace = Group.create!(name: name,
- path: name,
- owner: @current_user,
- parent: parent)
- namespace.add_owner(@current_user)
-
- namespace
- rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
- Namespace.where(parent: parent).find_by_path_or_name(name)
- end
- end
+ Groups::NestedCreateService.new(@current_user, group_path: names).execute
end
def full_path_namespace(names)
@@ -96,7 +80,7 @@ class GithubImport
end
def visibility_level
- @repo['private'] ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility
+ @repo['private'] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::CurrentSettings.current_application_settings.default_project_visibility
end
end
diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb
index 41dee5fdc06..4a3c40f88eb 100644
--- a/lib/uploaded_file.rb
+++ b/lib/uploaded_file.rb
@@ -27,7 +27,7 @@ class UploadedFile
alias_method :local_path, :path
def method_missing(method_name, *args, &block) #:nodoc:
- @tempfile.__send__(method_name, *args, &block)
+ @tempfile.__send__(method_name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def respond_to?(method_name, include_private = false) #:nodoc:
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index 85d806e6f20..fcd4aa29834 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -1,34 +1,51 @@
-# Huang Tao <htve@outlook.com>, 2017. #zanata
-# Lyubomir Vasilev <lyubomirv@abv.bg>, 2017. #zanata
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-13 12:07-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Bulgarian\n"
+"Language: bg_BG\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-08-03 04:43-0400\n"
-"Last-Translator: Lyubomir Vasilev <lyubomirv@abv.bg>\n"
-"Language-Team: Bulgarian (https://translate.zanata.org/project/view/GitLab)\n"
-"Language: bg\n"
-"X-Generator: Zanata 3.9.6\n"
-"Plural-Forms: nplurals=2; plural=(n != 1)\n"
-
-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 подаване беше пропуÑнато, за да не Ñе натоварва ÑиÑтемата."
-msgstr[1] "%s Ð¿Ð¾Ð´Ð°Ð²Ð°Ð½Ð¸Ñ Ð±Ñха пропуÑнати, за да не Ñе натоварва ÑиÑтемата."
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: bg\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d подаване"
msgstr[1] "%d подаваниÑ"
+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 подаване беше пропуÑнато, за да не Ñе натоварва ÑиÑтемата."
+msgstr[1] "%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."
+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 ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 Ñхема"
@@ -40,6 +57,18 @@ msgstr "Ðабор от графики отноÑно непрекъÑнатат
msgid "About auto deploy"
msgstr "ОтноÑно автоматичното внедрÑване"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr "Ðктивно"
@@ -56,35 +85,105 @@ msgid "Add License"
msgstr "ДобавÑне на лиценз"
msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr ""
-"Добавете SSH ключ в профила Ñи, за да можете да изтеглÑте или изпращате "
-"промени чрез SSH."
+msgstr "Добавете SSH ключ в профила Ñи, за да можете да изтеглÑте или изпращате промени чрез SSH."
msgid "Add new directory"
msgstr "ДобавÑне на нова папка"
+msgid "All"
+msgstr ""
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
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 "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Прикачете файл чрез влачене и пуÑкане или %{upload_link}"
+msgid "Authentication log"
+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 "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 ""
-"Клонът <strong>%{branch_name}</strong> беше Ñъздаден. За да наÑтроите "
-"автоматичното внедрÑване, изберете Yaml шаблон за GitLab CI и подайте "
-"промените Ñи. %{link_to_autodeploy_doc}"
+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 "Клонът <strong>%{branch_name}</strong> беше Ñъздаден. За да наÑтроите автоматичното внедрÑване, изберете Yaml шаблон за GitLab CI и подайте промените Ñи. %{link_to_autodeploy_doc}"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ТърÑете в клоновете"
@@ -110,12 +209,18 @@ msgstr "Разглеждане на файловете"
msgid "ByAuthor|by"
msgstr "от"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð½Ð° непрекъÑната интеграциÑ"
msgid "Cancel"
msgstr "Отказ"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Избиране в клона"
@@ -134,6 +239,9 @@ msgstr "СпиÑък Ñ Ð¿Ñ€Ð¾Ð¼ÐµÐ½Ð¸"
msgid "Charts"
msgstr "Графики"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Подбиране на това подаване"
@@ -194,6 +302,9 @@ msgstr "пропуÑнато"
msgid "CiStatus|running"
msgstr "протича в момента"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "Подаване"
@@ -226,12 +337,18 @@ msgstr "Подадено от"
msgid "Compare"
msgstr "Сравнение"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "РъководÑтво за ÑътрудничеÑтво"
msgid "Contributors"
msgstr "Сътрудници"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "Копиране на адреÑа в буфера за обмен"
@@ -241,12 +358,11 @@ msgstr "Копиране на идентификатора на подаване
msgid "Create New Directory"
msgstr "Създаване на нова папка"
-msgid ""
-"Create a personal access token on your account to pull or push via "
-"%{protocol}."
+msgid "Create a new branch"
msgstr ""
-"Създайте Ñи личен жетон за доÑтъп в профила Ñи, за да можете да изтеглÑте и "
-"изпращате промени чрез %{protocol}."
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr "Създайте Ñи личен жетон за доÑтъп в профила Ñи, за да можете да изтеглÑте и изпращате промени чрез %{protocol}."
msgid "Create directory"
msgstr "Създаване на папка"
@@ -278,24 +394,14 @@ msgstr "СинтакÑÐ¸Ñ Ð½Ð° „Cron“"
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 ""
-"ПерÑонализираните нива на извеÑÑ‚Ñване Ñа Ñъщите като нивата за учаÑтие. С "
-"перÑонализираните нива на извеÑÑ‚Ñване ще можете да получавате и извеÑÑ‚Ð¸Ñ Ð·Ð° "
-"избрани ÑъбитиÑ. За да научите повече, прегледайте %{notification_link}."
+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 "ПерÑонализираните нива на извеÑÑ‚Ñване Ñа Ñъщите като нивата за учаÑтие. С перÑонализираните нива на извеÑÑ‚Ñване ще можете да получавате и извеÑÑ‚Ð¸Ñ Ð·Ð° избрани ÑъбитиÑ. За да научите повече, прегледайте %{notification_link}."
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 "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 "Програмиране"
@@ -329,12 +435,21 @@ 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 "Да не Ñе показва повече"
@@ -371,6 +486,27 @@ msgstr "Редактиране"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Редактиране на плана %{id} за Ñхема"
+msgid "Emails"
+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 "Ð’Ñеки ден (в 4 ч. Ñутринта)"
@@ -416,8 +552,19 @@ 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 "Към Вашето разклонение"
@@ -425,29 +572,53 @@ msgstr "Към Вашето разклонение"
msgid "GoToYourFork|Fork"
msgstr "Разклонение"
+msgid "Group overview"
+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 "Home"
msgstr "Ðачало"
+msgid "Hooks"
+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 "Jobs for last month"
-msgstr "Задачи за поÑÐ»ÐµÐ´Ð½Ð¸Ñ Ð¼ÐµÑец"
-
-msgid "Jobs for last week"
-msgstr "Задачи за поÑледната Ñедмица"
+msgid "Issue events"
+msgstr ""
-msgid "Jobs for last year"
-msgstr "Задачи за поÑледната година"
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "Изключено"
@@ -455,6 +626,9 @@ msgstr "Изключено"
msgid "LFSStatus|Enabled"
msgstr "Включено"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "ПоÑÐ»ÐµÐ´Ð½Ð¸Ñ %d ден"
@@ -469,6 +643,12 @@ msgstr "ПоÑледна промÑна"
msgid "Last commit"
msgstr "ПоÑледно подаване"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "Ðаучете повече в"
@@ -481,17 +661,41 @@ 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] "Ограничено до показване на най-много %d Ñъбитие"
msgstr[1] "Ограничено до показване на най-много %d ÑъбитиÑ"
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "Медиана"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "добавите SSH ключ"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Ðов проблем"
@@ -590,6 +794,9 @@ msgstr "УчаÑтие"
msgid "NotificationLevel|Watch"
msgstr "Ðаблюдение"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Филтър"
@@ -599,9 +806,15 @@ msgstr "Отворен"
msgid "Options"
msgstr "Опции"
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
msgstr "СобÑтвеник"
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
msgstr "Схема"
@@ -614,6 +827,9 @@ msgstr "План за Ñхема"
msgid "Pipeline Schedules"
msgstr "Планове за Ñхема"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "ÐеуÑпешни:"
@@ -677,6 +893,15 @@ 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 "вÑички"
@@ -689,6 +914,15 @@ msgstr "Ñ ÐµÑ‚Ð°Ð¿"
msgid "Pipeline|with stages"
msgstr "Ñ ÐµÑ‚Ð°Ð¿Ð¸"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Проектът „%{project_name}“ е добавен в опашката за изтриване."
@@ -702,8 +936,10 @@ msgid "Project '%{project_name}' will be deleted."
msgstr "Проектът „%{project_name}“ ще бъде изтрит."
msgid "Project access must be granted explicitly to each user."
+msgstr "ДоÑтъпът до проекта Ñ‚Ñ€Ñбва да бъде даван поотделно на вÑеки потребител."
+
+msgid "Project details"
msgstr ""
-"ДоÑтъпът до проекта Ñ‚Ñ€Ñбва да бъде даван поотделно на вÑеки потребител."
msgid "Project export could not be deleted."
msgstr "ИзнеÑените данни на проекта не могат да бъдат изтрити."
@@ -711,20 +947,21 @@ 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 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 ""
-"ИзнаÑÑнето на проекта започна. Ще получите връзка към данните по е-поща."
+msgstr "ИзнаÑÑнето на проекта започна. Ще получите връзка към данните по е-поща."
msgid "Project home"
msgstr "Ðачална Ñтраница на проекта"
+msgid "Project overview"
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "Изключено"
@@ -746,6 +983,12 @@ msgstr "Етап"
msgid "ProjectNetworkGraph|Graph"
msgstr "Графика"
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Прочетете повече"
@@ -782,15 +1025,30 @@ 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 "Запазване на плана за Ñхема"
@@ -809,13 +1067,17 @@ msgstr "Изберете формата на архива"
msgid "Select a timezone"
msgstr "Изберете чаÑова зона"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "Изберете целеви клон"
-msgid "Set a password on your account to pull or push via %{protocol}."
+msgid "Service Templates"
msgstr ""
-"Задайте парола на профила Ñи, за да можете да изтеглÑте и изпращате промени "
-"чрез %{protocol}."
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr "Задайте парола на профила Ñи, за да можете да изтеглÑте и изпращате промени чрез %{protocol}."
msgid "Set up CI"
msgstr "ÐаÑтройка на ÐИ"
@@ -829,20 +1091,35 @@ msgstr "ÐаÑтройка на авт. внедрÑване"
msgid "SetPasswordToCloneLink|set a password"
msgstr "зададете парола"
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Показване на %d Ñъбитие"
msgstr[1] "Показване на %d ÑъбитиÑ"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "Изходен код"
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
msgid "StarProject|Star"
msgstr "Звезда"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Създайте %{new_merge_request} Ñ Ñ‚ÐµÐ·Ð¸ промени"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Преминаване към клон/етикет"
@@ -857,14 +1134,11 @@ msgstr "Етикети"
msgid "Target Branch"
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."
+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 "СъвкупноÑтта от ÑÑŠÐ±Ð¸Ñ‚Ð¸Ñ Ð´Ð¾Ð±Ð°Ð²ÐµÐ½Ð¸ към данните Ñъбрани за този етап."
@@ -872,46 +1146,20 @@ 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 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 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 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 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 "Ð’Ñеки впиÑан потребител има доÑтъп до проекта."
@@ -922,52 +1170,26 @@ 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 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 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 ""
-"Етапът на теÑтване показва времето, което е нужно на „Gitlab CI“ да изпълни "
-"вÑÑка Ñхема от задачи за Ñвързаната заÑвка за Ñливане. Данните ще бъдат "
-"добавени автоматично Ñлед като приключи изпълнението на първата Ви Ñхема."
+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 "Етапът на теÑтване показва времето, което е нужно на „Gitlab CI“ да изпълни вÑÑка Ñхема от задачи за Ñвързаната заÑвка за Ñливане. Данните ще бъдат добавени автоматично Ñлед като приключи изпълнението на първата Ви Ñхема."
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 ""
-"СтойноÑтта, коÑто Ñе намира в Ñредата на поÑледователноÑтта от наблюдавани "
-"данни. Ðапример: медианата на 3, 5 и 9 е 5, а медианата на 3, 5, 7 и 8 е "
-"(5+7)/2 = 6."
+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 "СтойноÑтта, коÑто Ñе намира в Ñредата на поÑледователноÑтта от наблюдавани данни. Ðапример: медианата на 3, 5 и 9 е 5, а медианата на 3, 5, 7 и 8 е (5+7)/2 = 6."
-msgid ""
-"This means you can not push code until you create an empty repository or "
-"import existing one."
+msgid "There are problems accessing Git storage: "
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 "Време преди един проблем да бъде планиран за работа"
@@ -976,8 +1198,7 @@ msgid "Time before an issue starts implementation"
msgstr "Време преди работата по проблем да започне"
msgid "Time between merge request creation and merge/close"
-msgstr ""
-"Време между Ñъздаване на заÑвка за Ñливане и прилагането/отхвърлÑнето Ñ"
+msgstr "Време между Ñъздаване на заÑвка за Ñливане и прилагането/отхвърлÑнето Ñ"
msgid "Time until first merge request"
msgstr "Време преди първата заÑвка за Ñливане"
@@ -1136,6 +1357,9 @@ msgstr "Качване на файл"
msgid "UploadLink|click to upload"
msgstr "щракнете за качване"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "Използване на глобалната Ви наÑтройка за извеÑтиÑта"
@@ -1160,40 +1384,23 @@ 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}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Ðа път Ñте да премахнете „%{group_name}“.\n"
-"Ðко Ñ Ð¿Ñ€ÐµÐ¼Ð°Ñ…Ð½ÐµÑ‚Ðµ, групата ÐЕ може да бъде възÑтановена!\n"
-"ÐÐИСТИÐРли иÑкате това?"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Ðа път Ñте да премахнете „%{group_name}“. Ðко Ñ Ð¿Ñ€ÐµÐ¼Ð°Ñ…Ð½ÐµÑ‚Ðµ, групата ÐЕ може да бъде възÑтановена! ÐÐИСТИÐРли иÑкате това?"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Ðа път Ñте да премахнете „%{project_name_with_namespace}“.\n"
-"Ðко го премахнете, той ÐЕ може да бъде възÑтановен!\n"
-"ÐÐИСТИÐРли иÑкате това?"
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Ðа път Ñте да премахнете „%{project_name_with_namespace}“. Ðко го премахнете, той ÐЕ може да бъде възÑтановен!ÐÐИСТИÐРли иÑкате това?"
-msgid ""
-"You are going to remove the fork relationship to source project "
-"%{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr ""
-"Ðа път Ñте да премахнете връзката на разклонението към Ð¾Ñ€Ð¸Ð³Ð¸Ð½Ð°Ð»Ð½Ð¸Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚, "
-"„%{forked_from_project}“. ÐÐИСТИÐРли иÑкате това?"
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr "Ðа път Ñте да премахнете връзката на разклонението към Ð¾Ñ€Ð¸Ð³Ð¸Ð½Ð°Ð»Ð½Ð¸Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚, „%{forked_from_project}“. ÐÐИСТИÐРли иÑкате това?"
-msgid ""
-"You are going to transfer %{project_name_with_namespace} to another owner. "
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Ðа път Ñте да прехвърлите „%{project_name_with_namespace}“ към друг "
-"ÑобÑтвеник. ÐÐИСТИÐРли иÑкате това?"
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr "Ðа път Ñте да прехвърлите „%{project_name_with_namespace}“ към друг ÑобÑтвеник. ÐÐИСТИÐРли иÑкате това?"
msgid "You can only add files when you are on a branch"
msgstr "Можете да добавÑте файлове Ñамо когато Ñе намирате в клон"
@@ -1213,31 +1420,20 @@ 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"
+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"
+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 ""
-"ÐÑма да можете да изтеглÑте или изпращате код в проекта чрез %{protocol}, "
-"докато не %{set_password_link} за профила Ñи"
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr "ÐÑма да можете да изтеглÑте или изпращате код в проекта чрез %{protocol}, докато не %{set_password_link} за профила Ñи"
-msgid ""
-"You won't be able to pull or push project code via SSH until you "
-"%{add_ssh_key_link} to your profile"
-msgstr ""
-"ÐÑма да можете да изтеглÑте или изпращате код в проекта чрез SSH, докато не "
-"%{add_ssh_key_link} в профила Ñи"
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr "ÐÑма да можете да изтеглÑте или изпращате код в проекта чрез SSH, докато не %{add_ssh_key_link} в профила Ñи"
msgid "Your name"
msgstr "Вашето име"
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index ea864091b10..86deb620f0b 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -1,56 +1,422 @@
-# German translations for gitlab package.
-# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the gitlab package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
-#
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"PO-Revision-Date: 2017-05-09 13:44+0200\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: German\n"
-"Language: de\n"
+"Language: de_DE\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"
-"Last-Translator: \n"
-"X-Generator: Poedit 2.0.1\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: de\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
+
+msgid "%d commit"
+msgid_plural "%d commits"
+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] "%s zusätzlicher Commit wurde ausgelassen um Leistungsprobleme zu verhindern."
+msgstr[1] "%s zusätzliche Commits wurden ausgelassen um Leistungsprobleme zu verhindern."
+
+msgid "%{commit_author_link} committed %{commit_timeago}"
+msgstr ""
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
+msgstr "%{number_of_failures} von %{maximum_failures} Fehlschlägen. GitLab wird den Zugriff beim nächsten Versuch zulassen."
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds."
+msgstr "%{number_of_failures} von %{maximum_failures} Fehlschlägen. GitLab wird den Zugriff für %{number_of_seconds} Sekunden blockieren."
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
+msgstr "%{number_of_failures} von %{maximum_failures} Fehlschlägen. GitLab wird es nicht weiter versuchen. Setze die Speicherinformation nach Behebung des Problems zurück."
+
+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 "(beachte die Informationen zur Installation auf %{link})."
+
+msgid "1 pipeline"
+msgid_plural "%d pipelines"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "A collection of graphs regarding Continuous Integration"
+msgstr "Eine Sammlung von Graphen bezüglich kontinuierlicher Integration"
+
+msgid "About auto deploy"
+msgstr "Ãœber automatische Bereitstellung "
+
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 "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 ""
+
+msgid "Active"
+msgstr ""
+
+msgid "Activity"
+msgstr ""
+
+msgid "Add Changelog"
+msgstr "Änderungsliste hinzufügen "
+
+msgid "Add Contribution guide"
+msgstr "Mitarbeitsanleitung hinzufügen"
+
+msgid "Add License"
+msgstr ""
+
+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)."
+
+msgid "Add new directory"
+msgstr "Erstelle eine neues Verzeichnis"
+
+msgid "All"
+msgstr "Alle"
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
+msgid "Archived project! Repository is read-only"
+msgstr "Archiviertes Projekt! Repository ist nicht änderbar."
msgid "Are you sure you want to delete this pipeline schedule?"
+msgstr "Bist Du sicher, dass Du diesen Pipeline-Zeitplan löschen möchtest?"
+
+msgid "Are you sure you want to discard your changes?"
+msgstr "Bist Du sicher, dass Du alle Änderungen zurücksetzen willst?"
+
+msgid "Are you sure you want to reset registration token?"
+msgstr "Bist Du sicher, dass Du den Registrierungstoken zurücksetzen willst?"
+
+msgid "Are you sure you want to reset the health check token?"
+msgstr "Bist Du sicher, dass Du den Systemüberwachungstoken zurücksetzen willst?"
+
+msgid "Are you sure?"
+msgstr "Bist Du sicher?"
+
+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"
+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 "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 "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}"
+
+msgid "BranchSwitcherPlaceholder|Search branches"
+msgstr "Branches durchsuchen"
+
+msgid "BranchSwitcherTitle|Switch branch"
+msgstr "Branch wechseln"
+
+msgid "Branches"
+msgstr ""
+
+msgid "Browse Directory"
+msgstr "Verzeichnisse durchsuchen"
+
+msgid "Browse File"
+msgstr "Datei durchsuchen"
+
+msgid "Browse Files"
+msgstr "Dateien durchsuchen"
+
+msgid "Browse files"
+msgstr "Dateien durchsuchen"
+
msgid "ByAuthor|by"
-msgstr "Von"
+msgstr "von"
+
+msgid "CI / CD"
+msgstr ""
+
+msgid "CI configuration"
+msgstr ""
msgid "Cancel"
msgstr ""
+msgid "Cancel edit"
+msgstr "Bearbeitung abbrechen"
+
+msgid "ChangeTypeActionLabel|Pick into branch"
+msgstr "In dem Branch wählen"
+
+msgid "ChangeTypeActionLabel|Revert in branch"
+msgstr "Im Branch wiederherstellen"
+
+msgid "ChangeTypeAction|Cherry-pick"
+msgstr "Herauspicken"
+
+msgid "ChangeTypeAction|Revert"
+msgstr "Wiederherstellen "
+
+msgid "Changelog"
+msgstr "Änderungsliste "
+
+msgid "Charts"
+msgstr "Diagramme"
+
+msgid "Chat"
+msgstr ""
+
+msgid "Cherry-pick this commit"
+msgstr "Diesen Commit herauspicken "
+
+msgid "Cherry-pick this merge request"
+msgstr "Diesen Merge Request herauspicken"
+
+msgid "CiStatusLabel|canceled"
+msgstr "abgebrochen"
+
+msgid "CiStatusLabel|created"
+msgstr "erstellt"
+
+msgid "CiStatusLabel|failed"
+msgstr "fehlgeschlagen"
+
+msgid "CiStatusLabel|manual action"
+msgstr "manuelles Eingreifen"
+
+msgid "CiStatusLabel|passed"
+msgstr "absolviert"
+
+msgid "CiStatusLabel|passed with warnings"
+msgstr "mit Warnungen absolviert"
+
+msgid "CiStatusLabel|pending"
+msgstr "ausstehend"
+
+msgid "CiStatusLabel|skipped"
+msgstr "übersprungen"
+
+msgid "CiStatusLabel|waiting for manual action"
+msgstr "wartet auf manuelles Eingreifen"
+
+msgid "CiStatusText|blocked"
+msgstr "blockiert"
+
+msgid "CiStatusText|canceled"
+msgstr "abgebrochen"
+
+msgid "CiStatusText|created"
+msgstr "erstellt"
+
+msgid "CiStatusText|failed"
+msgstr "fehlgeschlagen"
+
+msgid "CiStatusText|manual"
+msgstr "manuell"
+
+msgid "CiStatusText|passed"
+msgstr "absolviert"
+
+msgid "CiStatusText|pending"
+msgstr "ausstehend"
+
+msgid "CiStatusText|skipped"
+msgstr "übersprungen"
+
+msgid "CiStatus|running"
+msgstr "laufend"
+
+msgid "Comments"
+msgstr "Kommentare"
+
msgid "Commit"
msgid_plural "Commits"
-msgstr[0] "Commit"
-msgstr[1] "Commits"
+msgstr[0] ""
+msgstr[1] ""
-msgid "Cron Timezone"
+msgid "Commit duration in minutes for last 30 commits"
+msgstr "Dauer der Commits in Minuten für die letzten 30 Commits"
+
+msgid "Commit message"
+msgstr "Commit Nachricht"
+
+msgid "CommitBoxTitle|Commit"
+msgstr "Commit"
+
+msgid "CommitMessage|Add %{file_name}"
+msgstr "%{file_name} hinzufügen"
+
+msgid "Commits"
msgstr ""
+msgid "Commits feed"
+msgstr "Liste der Commits"
+
+msgid "Commits|History"
+msgstr "Verlauf"
+
+msgid "Committed by"
+msgstr "Committed von"
+
+msgid "Compare"
+msgstr "Vergleichen"
+
+msgid "Container Registry"
+msgstr ""
+
+msgid "Contribution guide"
+msgstr "Mitarbeitsanleitung"
+
+msgid "Contributors"
+msgstr "Mitarbeiter"
+
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
+msgid "Copy URL to clipboard"
+msgstr "Kopiere URL in die Zwischenablage"
+
+msgid "Copy commit SHA to clipboard"
+msgstr "Kopiere Commit SHA in die Zwischenablage"
+
+msgid "Create New Directory"
+msgstr "Erstelle neues Verzeichnis"
+
+msgid "Create a new branch"
+msgstr "Erstelle einen neuen Branch"
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr "Erstelle einen persönlichen Zugriffstoken in Deinem Konto um mittels %{protocol} zu übertragen (push) oder abzurufen (pull)."
+
+msgid "Create directory"
+msgstr "Erstelle Verzeichnis"
+
+msgid "Create empty bare repository"
+msgstr "Erstelle leeres Repository"
+
+msgid "Create merge request"
+msgstr "Erstelle Merge Request"
+
+msgid "Create new..."
+msgstr "Erstelle neues..."
+
+msgid "CreateNewFork|Fork"
+msgstr "Ableger"
+
+msgid "CreateTag|Tag"
+msgstr "Tag "
+
+msgid "CreateTokenToCloneLink|create a personal access token"
+msgstr "Erstelle einen persönlichen Zugriffstoken"
+
+msgid "Cron Timezone"
+msgstr "Cron Zeitzone"
+
+msgid "Cron syntax"
+msgstr "Cron Syntax"
+
+msgid "Custom notification events"
+msgstr "Individuelle Benachrichtigungsereignisse"
+
+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 "Individuelle Benachrichtigungsstufen sind identisch mit den Beteiligungsstufen. Mit individuellen Benachrichtigungsstufen erhältst Du ebenfalls Mitteilungen für ausgewählte Ereignisse. Für weitere Informationen lies %{notification_link}. "
+
+msgid "Cycle Analytics"
+msgstr "Arbeitsablaufsanalysen"
+
msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
-msgstr "Cycle Analytics liefern einen Überblick darüber, wie viel Zeit in Ihrem Projekt von einer Idee bis zum Produktivdeployment vergeht."
+msgstr "Arbeitsablaufsanalysen verschaffen einen Überblick, welche Zeit Dein Projekt von der Idee zur Realisierung benötigt."
msgid "CycleAnalyticsStage|Code"
-msgstr "Code"
+msgstr "Entwicklung"
msgid "CycleAnalyticsStage|Issue"
-msgstr "Issue"
+msgstr "Ticket"
msgid "CycleAnalyticsStage|Plan"
msgstr "Planung"
msgid "CycleAnalyticsStage|Production"
-msgstr "Produktiv"
+msgstr "Produktion"
msgid "CycleAnalyticsStage|Review"
-msgstr "Review"
+msgstr "Überprüfung"
msgid "CycleAnalyticsStage|Staging"
msgstr "Staging"
@@ -58,76 +424,315 @@ msgstr "Staging"
msgid "CycleAnalyticsStage|Test"
msgstr "Test"
+msgid "Define a custom pattern with cron syntax"
+msgstr "Erstelle ein individuelles Muster mittels Cron Syntax"
+
msgid "Delete"
-msgstr ""
+msgstr "Löschen"
msgid "Deploy"
msgid_plural "Deploys"
-msgstr[0] "Deployment"
-msgstr[1] "Deployments"
+msgstr[0] "Bereitstellung"
+msgstr[1] "Bereitstellungen"
+
+msgid "Deploy Keys"
+msgstr ""
msgid "Description"
+msgstr "Beschreibung"
+
+msgid "Details"
msgstr ""
+msgid "Directory name"
+msgstr "Verzeichnisname"
+
+msgid "Discard changes"
+msgstr "Änderungen verwerfen"
+
+msgid "Don't show again"
+msgstr "Nicht erneut anzeigen"
+
+msgid "Download"
+msgstr "Herunterladen"
+
+msgid "Download tar"
+msgstr "TAR-Datei herunterladen"
+
+msgid "Download tar.bz2"
+msgstr "TAR.BZ2-Datei herunterladen"
+
+msgid "Download tar.gz"
+msgstr "TAR.GZ-Datei herunterladen"
+
+msgid "Download zip"
+msgstr "ZIP-Datei herunterladen"
+
+msgid "DownloadArtifacts|Download"
+msgstr "Herunterladen"
+
+msgid "DownloadCommit|Email Patches"
+msgstr "E-Mail Patch"
+
+msgid "DownloadCommit|Plain Diff"
+msgstr "Unterschiede"
+
+msgid "DownloadSource|Download"
+msgstr "Herunterladen"
+
msgid "Edit"
-msgstr ""
+msgstr "Bearbeiten"
msgid "Edit Pipeline Schedule %{id}"
+msgstr "Pipeline Zeitplan bearbeiten %{id}"
+
+msgid "Emails"
msgstr ""
+msgid "EventFilterBy|Filter by all"
+msgstr "Filtere alle"
+
+msgid "EventFilterBy|Filter by comments"
+msgstr "Filtere nach Kommentaren"
+
+msgid "EventFilterBy|Filter by issue events"
+msgstr "Filtere nach Tickets"
+
+msgid "EventFilterBy|Filter by merge events"
+msgstr "Filtere nach Merge Requests"
+
+msgid "EventFilterBy|Filter by push events"
+msgstr "Filtere nach Ãœbertragungen"
+
+msgid "EventFilterBy|Filter by team"
+msgstr "Filtere nach Teams"
+
+msgid "Every day (at 4:00am)"
+msgstr "Täglich (um 4:00 Uhr)"
+
+msgid "Every month (on the 1st at 4:00am)"
+msgstr "Monatlich (am Ersten um 4:00 Uhr)"
+
+msgid "Every week (Sundays at 4:00am)"
+msgstr "Wöchentlich (Sonntags um 4:00 Uhr)"
+
msgid "Failed to change the owner"
-msgstr ""
+msgstr "Wechsel des Besitzers fehlgeschlagen"
msgid "Failed to remove the pipeline schedule"
-msgstr ""
+msgstr "Entfernung der Pipelineplanung fehlgeschlagen"
-msgid "Filter"
-msgstr ""
+msgid "Files"
+msgstr "Dateien"
+
+msgid "Filter by commit message"
+msgstr "Filter nach Commit Nachricht"
+
+msgid "Find by path"
+msgstr "Finde über den Pfad"
+
+msgid "Find file"
+msgstr "Finde Datei"
msgid "FirstPushedBy|First"
msgstr "Erster"
msgid "FirstPushedBy|pushed by"
-msgstr "gepusht von"
+msgstr "übertragen von"
+
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] "Ableger"
+msgstr[1] "Ableger"
+
+msgid "ForkedFromProjectPath|Forked from"
+msgstr "Ableger von"
msgid "From issue creation until deploy to production"
-msgstr "Vom Anlegen des Issues bis zum Produktivdeployment"
+msgstr "Von der Ticketbeschreibung bis zur Bereitstellung"
msgid "From merge request merge until deploy to production"
-msgstr "Vom Merge Request bis zum Produktivdeployment"
+msgstr "Vom Umsetzen des Merge Request bis zur Bereitstellung auf dem Produktivsystem"
-msgid "Interval Pattern"
+msgid "GPG Keys"
+msgstr ""
+
+msgid "Geo Nodes"
+msgstr ""
+
+msgid "Git storage health information has been reset"
+msgstr "Informationen über den Speicherzustand von Gitlab wurden zurückgesetzt."
+
+msgid "GitLab Runner section"
+msgstr "GitLab Runner Bereich"
+
+msgid "Go to your fork"
+msgstr "Gehe zu Deinem Ableger"
+
+msgid "GoToYourFork|Fork"
+msgstr "Ableger"
+
+msgid "Group overview"
+msgstr ""
+
+msgid "Health Check"
+msgstr "Systemzustand"
+
+msgid "Health information can be retrieved from the following endpoints. More information is available"
+msgstr "Informationen über den Systemzustand können von folgenden Endpunkten erhalten werden. Mehr Informationen gibt es"
+
+msgid "HealthCheck|Access token is"
+msgstr "Zugriffstoken ist"
+
+msgid "HealthCheck|Healthy"
+msgstr "OK"
+
+msgid "HealthCheck|No Health Problems Detected"
+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"
+
+msgid "Import repository"
+msgstr "Repository importieren"
+
+msgid "Install a Runner compatible with GitLab CI"
+msgstr "Installiere einen Runner der mit GitLab CI kompatibel ist"
+
+msgid "Interval Pattern"
+msgstr "Intervallmuster"
+
msgid "Introducing Cycle Analytics"
-msgstr "Was sind Cycle Analytics?"
+msgstr "Arbeitsablaufsanalysen vorgestellt"
+
+msgid "Issue events"
+msgstr "Ticketereignisse"
+
+msgid "Issues"
+msgstr ""
+
+msgid "LFSStatus|Disabled"
+msgstr "Deaktiviert"
+
+msgid "LFSStatus|Enabled"
+msgstr "Aktiviert"
+
+msgid "Labels"
+msgstr ""
msgid "Last %d day"
msgid_plural "Last %d days"
-msgstr[0] "Letzter %d Tag"
+msgstr[0] "Letzten %d Tag"
msgstr[1] "Letzten %d Tage"
msgid "Last Pipeline"
+msgstr "Letzte Pipeline"
+
+msgid "Last Update"
+msgstr "Letzte Aktualisierung"
+
+msgid "Last commit"
+msgstr "Letzter Commit"
+
+msgid "LastPushEvent|You pushed to"
+msgstr "Du übertrugst an"
+
+msgid "LastPushEvent|at"
+msgstr "am"
+
+msgid "Learn more in the"
+msgstr "Erfahre mehr in den"
+
+msgid "Learn more in the|pipeline schedules documentation"
+msgstr "Pipelineplanungsdokumentation"
+
+msgid "Leave group"
+msgstr "Verlasse die Gruppe"
+
+msgid "Leave project"
+msgstr "Verlasse das Projekt"
+
+msgid "License"
msgstr ""
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
-msgstr[0] "Eingeschränkt auf maximal %d Ereignis"
-msgstr[1] "Eingeschränkt auf maximal %d Ereignisse"
+msgstr[0] "Limitiere die Anzeige auf höchstens %d Ereignis"
+msgstr[1] "Limitiere die Anzeige auf höchstens %d Ereignisse"
+
+msgid "Locked Files"
+msgstr ""
msgid "Median"
-msgstr "Median"
+msgstr ""
+
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr "Ereignisse zusammenführen"
+
+msgid "Messages"
+msgstr ""
+
+msgid "MissingSSHKeyWarningLink|add an SSH key"
+msgstr "einen SSH Schlüssel hinzufügst"
+
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr "hier"
msgid "New Issue"
msgid_plural "New Issues"
-msgstr[0] "Neues Issue"
-msgstr[1] "Neue Issues"
+msgstr[0] "Neues Ticket"
+msgstr[1] "Neue Tickets"
msgid "New Pipeline Schedule"
-msgstr ""
+msgstr "Neuer Pipeline Zeitplan"
+
+msgid "New branch"
+msgstr "Neuer Branch"
+
+msgid "New directory"
+msgstr "Neues Verzeichnis"
+
+msgid "New file"
+msgstr "Neue Datei"
+
+msgid "New issue"
+msgstr "Neues Ticket"
+
+msgid "New merge request"
+msgstr "Neuer Merge Request"
+
+msgid "New schedule"
+msgstr "Neuer Zeitplan"
+
+msgid "New snippet"
+msgstr "Neuer Schnipsel"
+
+msgid "New tag"
+msgstr "Neuer Tag"
+
+msgid "No repository"
+msgstr "Kein Repository"
msgid "No schedules"
-msgstr ""
+msgstr "Keine Zeitpläne"
msgid "Not available"
msgstr "Nicht verfügbar"
@@ -135,62 +740,275 @@ msgstr "Nicht verfügbar"
msgid "Not enough data"
msgstr "Nicht genügend Daten"
+msgid "Notification events"
+msgstr "Benachrichtigungsereignisse"
+
+msgid "NotificationEvent|Close issue"
+msgstr "Ticket abschließen"
+
+msgid "NotificationEvent|Close merge request"
+msgstr "Merge Request abschließen"
+
+msgid "NotificationEvent|Failed pipeline"
+msgstr "Fehlgeschlagene Pipeline"
+
+msgid "NotificationEvent|Merge merge request"
+msgstr "Merge Request umsetzen"
+
+msgid "NotificationEvent|New issue"
+msgstr "Neues Ticket"
+
+msgid "NotificationEvent|New merge request"
+msgstr "Neuer Merge Request"
+
+msgid "NotificationEvent|New note"
+msgstr "Neue Notiz"
+
+msgid "NotificationEvent|Reassign issue"
+msgstr "Ticket neu zuweisen"
+
+msgid "NotificationEvent|Reassign merge request"
+msgstr "Merge Request neu zuweisen"
+
+msgid "NotificationEvent|Reopen issue"
+msgstr "Ticket wieder öffnen"
+
+msgid "NotificationEvent|Successful pipeline"
+msgstr "Erfolgreiche Pipeline"
+
+msgid "NotificationLevel|Custom"
+msgstr "Individuell"
+
+msgid "NotificationLevel|Disabled"
+msgstr "Deaktiviert"
+
+msgid "NotificationLevel|Global"
+msgstr "Global"
+
+msgid "NotificationLevel|On mention"
+msgstr "Zum Vermerk"
+
+msgid "NotificationLevel|Participate"
+msgstr "Teilnehmen"
+
+msgid "NotificationLevel|Watch"
+msgstr "Beobachten"
+
+msgid "Notifications"
+msgstr ""
+
+msgid "OfSearchInADropdown|Filter"
+msgstr "Filter"
+
msgid "OpenedNDaysAgo|Opened"
-msgstr "Erstellt"
+msgstr "Ungelöst"
+
+msgid "Options"
+msgstr "Optionen"
+
+msgid "Overview"
+msgstr ""
msgid "Owner"
+msgstr "Besitzer"
+
+msgid "Password"
+msgstr ""
+
+msgid "Pipeline"
msgstr ""
msgid "Pipeline Health"
-msgstr "Pipeline Kennzahlen"
+msgstr "Zustand der Pipeline"
msgid "Pipeline Schedule"
-msgstr ""
+msgstr "Zeitplan der Pipeline"
msgid "Pipeline Schedules"
+msgstr "Zustände der Pipeline"
+
+msgid "Pipeline quota"
msgstr ""
+msgid "PipelineCharts|Failed:"
+msgstr "Fehlgeschlagen:"
+
+msgid "PipelineCharts|Overall statistics"
+msgstr "Gesamte Statisktiken"
+
+msgid "PipelineCharts|Success ratio:"
+msgstr "Erfolgsverhältnis:"
+
+msgid "PipelineCharts|Successful:"
+msgstr "Erfolgreich:"
+
+msgid "PipelineCharts|Total:"
+msgstr "Insgesamt:"
+
msgid "PipelineSchedules|Activated"
-msgstr ""
+msgstr "Aktiviert"
msgid "PipelineSchedules|Active"
-msgstr ""
+msgstr "Aktiv"
msgid "PipelineSchedules|All"
-msgstr ""
+msgstr "Alle"
msgid "PipelineSchedules|Inactive"
-msgstr ""
+msgstr "Inaktiv"
+
+msgid "PipelineSchedules|Input variable key"
+msgstr "Schlüssel der Eingangsvariable"
+
+msgid "PipelineSchedules|Input variable value"
+msgstr "Wert der Eingangsvariable"
msgid "PipelineSchedules|Next Run"
-msgstr ""
+msgstr "Nächste Durchführung"
msgid "PipelineSchedules|None"
-msgstr ""
+msgstr "Nichts"
msgid "PipelineSchedules|Provide a short description for this pipeline"
-msgstr ""
+msgstr "Beschreibe diese Pipeline"
+
+msgid "PipelineSchedules|Remove variable row"
+msgstr "Entferne Variablenreihe"
msgid "PipelineSchedules|Take ownership"
-msgstr ""
+msgstr "Eigentümer werden"
msgid "PipelineSchedules|Target"
+msgstr "Ziel"
+
+msgid "PipelineSchedules|Variables"
+msgstr "Variablen"
+
+msgid "PipelineSheduleIntervalPattern|Custom"
+msgstr "Individuell"
+
+msgid "Pipelines"
+msgstr ""
+
+msgid "Pipelines charts"
+msgstr "Pipelinediagramme"
+
+msgid "Pipelines for last month"
+msgstr ""
+
+msgid "Pipelines for last week"
+msgstr ""
+
+msgid "Pipelines for last year"
+msgstr ""
+
+msgid "Pipeline|all"
+msgstr "Alle"
+
+msgid "Pipeline|success"
+msgstr "Erfolg"
+
+msgid "Pipeline|with stage"
+msgstr "mit Stage"
+
+msgid "Pipeline|with stages"
+msgstr "mit Stages"
+
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr "Projekt"
+
+msgid "Project '%{project_name}' queued for deletion."
+msgstr "Das Projekt '%{project_name}' wurde zur Löschung eingeplant."
+
+msgid "Project '%{project_name}' was successfully created."
+msgstr "Das Projekt '%{project_name}' wurde erfolgreich erstellt."
+
+msgid "Project '%{project_name}' was successfully updated."
+msgstr "Das Projekt '%{project_name}' wurde erfolgreich aktualisiert."
+
+msgid "Project '%{project_name}' will be deleted."
+msgstr "Das Projekt '%{project_name}' wird gelöscht."
+
+msgid "Project access must be granted explicitly to each user."
+msgstr "Jedem Nutzer muss explizit der Zugriff auf das Projekt gewährt werden."
+
+msgid "Project details"
+msgstr "Projektdetails"
+
+msgid "Project export could not be deleted."
+msgstr "Der Export des Projekts konnte nich gelöscht werden."
+
+msgid "Project export has been deleted."
+msgstr "Der Export des Projekts wurde gelöscht."
+
+msgid "Project export link has expired. Please generate a new export from your project settings."
+msgstr "Der Link für den Export des Projektes ist abgelaufen. Bitte generiere einen neuen Export in den Projekteinstellungen."
+
+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"
+
+msgid "ProjectFeature|Disabled"
+msgstr "Dekativiert"
+
+msgid "ProjectFeature|Everyone with access"
+msgstr "Jeder mit Zugriff"
+
+msgid "ProjectFeature|Only team members"
+msgstr "Nur Teammitglieder"
+
+msgid "ProjectFileTree|Name"
+msgstr "Name"
+
+msgid "ProjectLastActivity|Never"
+msgstr "Niemals"
+
msgid "ProjectLifecycle|Stage"
-msgstr "Phase"
+msgstr "Stage"
+
+msgid "ProjectNetworkGraph|Graph"
+msgstr "Diagramm"
+
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr "Ãœbertragungsereignisse"
msgid "Read more"
-msgstr "Mehr"
+msgstr "Mehr lesen"
+
+msgid "Readme"
+msgstr "Lies mich"
+
+msgid "RefSwitcher|Branches"
+msgstr "Branches"
+
+msgid "RefSwitcher|Tags"
+msgstr "Tags"
msgid "Related Commits"
msgstr "Zugehörige Commits"
msgid "Related Deployed Jobs"
-msgstr "Zugehörige Deploymentjobs"
+msgstr "Zugehörige ausgelieferte Jobs"
msgid "Related Issues"
-msgstr "Zugehörige Issues"
+msgstr "Zugehörige Tickets"
msgid "Related Jobs"
msgstr "Zugehörige Jobs"
@@ -199,18 +1017,81 @@ msgid "Related Merge Requests"
msgstr "Zugehörige Merge Requests"
msgid "Related Merged Requests"
-msgstr "Zugehörige abgeschlossene Merge Requests"
+msgstr "Zugehörige umgesetzte Merge Requests"
-msgid "Save pipeline schedule"
+msgid "Remind later"
+msgstr "Später erinnern"
+
+msgid "Remove project"
+msgstr "Projekt entfernen"
+
+msgid "Repository"
msgstr ""
-msgid "Schedule a new pipeline"
+msgid "Request Access"
+msgstr "Anfrage auf Zugriff"
+
+msgid "Reset git storage health information"
+msgstr "Informationen über Speicherzustand zurücksetzen"
+
+msgid "Reset health check access token"
+msgstr "Zugriffstoken für Systemzustand zurücksetzen"
+
+msgid "Reset runners registration token"
+msgstr "Registrierungstoken für Runner zurücksetzen"
+
+msgid "Revert this commit"
+msgstr "Commit zurücksetzen"
+
+msgid "Revert this merge request"
+msgstr "Merge Request zurücksetzen"
+
+msgid "SSH Keys"
msgstr ""
+msgid "Save pipeline schedule"
+msgstr "Zeitplan der Pipeline speichern"
+
+msgid "Schedule a new pipeline"
+msgstr "Plane eine neue Pipeline"
+
+msgid "Scheduling Pipelines"
+msgstr "Pipelines planen"
+
+msgid "Search branches and tags"
+msgstr "Suche nach Branches und Tags"
+
+msgid "Select Archive Format"
+msgstr "Archivierungsformat auswählen"
+
msgid "Select a timezone"
-msgstr ""
+msgstr "Zeitzone auswählen"
+
+msgid "Select existing branch"
+msgstr "Existierenden Branch auswählen"
msgid "Select target branch"
+msgstr "Zielbranch auswählen"
+
+msgid "Service Templates"
+msgstr ""
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr "Lege ein Passwort für dein Konto fest, um mittels %{protocol} zu übertragen (push) oder abzurufen (pull)."
+
+msgid "Set up CI"
+msgstr "CI einrichten"
+
+msgid "Set up Koding"
+msgstr "Koding einrichten"
+
+msgid "Set up auto deploy"
+msgstr "Automatische Bereitstellung einrichten"
+
+msgid "SetPasswordToCloneLink|set a password"
+msgstr "ein Passwort festlegst"
+
+msgid "Settings"
msgstr ""
msgid "Showing %d event"
@@ -218,86 +1099,358 @@ msgid_plural "Showing %d events"
msgstr[0] "Zeige %d Ereignis"
msgstr[1] "Zeige %d Ereignisse"
+msgid "Snippets"
+msgstr ""
+
+msgid "Source code"
+msgstr "Quellcode"
+
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr "Lege die folgende URL während des Runner Setups fest:"
+
+msgid "StarProject|Star"
+msgstr "Favorisieren"
+
+msgid "Start a %{new_merge_request} with these changes"
+msgstr "Beginne einen %{new_merge_request} mit diesen Änderungen"
+
+msgid "Start the Runner!"
+msgstr "Starte den Runner!"
+
+msgid "Switch branch/tag"
+msgstr "Zu Branch/Tag wechseln"
+
+msgid "Tag"
+msgid_plural "Tags"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Tags"
+msgstr ""
+
msgid "Target Branch"
+msgstr "Zielbranch"
+
+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 "Die Code-Phase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Sie Ihren ersten Merge Request anlegen, werden dessen Daten automatisch ergänzt."
+msgstr "Die Entwicklungsphase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Du Deinen ersten Merge Request anlegst, werden dessen Daten automatisch ergänzt."
msgid "The collection of events added to the data gathered for that stage."
msgstr "Ereignisse, die für diese Phase ausgewertet wurden."
+msgid "The fork relationship has been removed."
+msgstr "Die Beziehung des Ablegers wurde entfernt."
+
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 "Die Issue-Phase stellt die Zeit vom Anlegen eines Issues bis zum Zuweisen eines Meilensteins oder Hinzufügen zum Issue Board dar. Erstellen Sie einen Issue, damit dessen Daten hier erscheinen."
+msgstr "Die Ticketphase stellt die Zeit vom Anlegen eines Tickets bis zum Zuweisen eines Meilensteins oder Hinzufügen zur Aufgabentafel dar. Erstelle einen Ticket, damit dessen Daten hier erscheinen."
msgid "The phase of the development lifecycle."
-msgstr "Die Phase im Entwicklungsprozess."
+msgstr "Die Phase des Entwicklungslebenszyklus."
+
+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 "Die Pipelinezeitpläne starten Pipelines in der Zukunft, wiederholend, für bestimmte Branches oder Tags. Diese geplanten Pipelines haben denselben begrenzten Zugriff auf das Projekt, wie der zugeordnete Nutzer."
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 "Die Planungsphase stellt die Zeit von der vorherigen Phase bis zum Pushen des ersten Commits dar. Sobald Sie den ersten Commit pushen, werden dessen Daten hier erscheinen."
+msgstr "Die Planungsphase stellt die Zeit von der vorherigen Phase bis zum Übertragen des ersten Commits dar. Sobald Du den ersten Commit überträgst, werden dessen Daten hier erscheinen."
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 "Die Produktiv-Phase stellt die Gesamtzeit vom Anlegen eines Issues bis zum Deployment auf dem Produktivsystem dar. Sobald Sie den vollständigen Entwicklungszyklus von einer Idee bis zum Produktivdeployment durchlaufen haben, erscheinen die zugehörigen Daten hier."
+msgstr "Die Produktionsphase stellt die Gesamtzeit vom Anlegen eines Tickets bis zur Bereitstellung des Codes auf dem Produktivsystem dar. Sobald Du den vollständigen Entwicklungszyklus, von einer Idee bis zur Fertigstellung, durchlaufen hast, erscheinen die zugehörigen Daten hier."
+
+msgid "The project can be accessed by any logged in user."
+msgstr "Auf das Projekt kann jeder angemeldete Nutzer zugreifen."
+
+msgid "The project can be accessed without any authentication."
+msgstr "Auf das Projekt kann ohne Authentifizierung zugegriffen werden."
+
+msgid "The repository for this project does not exist."
+msgstr "Das Repository für das Projekt existiert nicht."
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 "Die Review-Phase stellt die Zeit vom Anlegen eines Merge Requests bis zum Mergen dar. Sobald Sie Ihren ersten Merge Request abschließen, werden dessen Daten hier automatisch angezeigt."
+msgstr "Die Überprüfungsphase stellt die Zeit vom Anlegen eines Merge Requests bis dessen Umsetzung dar. Sobald Du Deinen ersten Merge Request abschließt, werden dessen Daten hier automatisch angezeigt."
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 "Die Staging-Phase stellt die Zeit zwischen Mergen eines Merge Requests und dem Produktivdeployment dar. Sobald Sie das erste Produktivdeployment durchgeführt haben, werden dessen Daten hier automatisch angezeigt."
+msgstr "Die Staging-Phase stellt die Zeit zwischen der Umsetzung eines Merge Requests und der Bereitstellung des Codes auf dem Produktivsystem dar. Sobald Du das erste Mal auf das Produktivsystem ausgeliefert hast, werden dessen Daten hier automatisch angezeigt."
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 "Die Test-Phase stellt die Zeit dar, die GitLab CI benötigt um die Pipelines von Merge Requests abzuarbeiten. Sobald die erste Pipeline abgeschlossen ist, werden deren Daten hier automatisch angezeigt."
+msgstr "Die Testphase stellt die Zeit dar, die GitLab CI benötigt um die Pipelines von zugehörigen Merge Requests abzuarbeiten. Sobald die erste Pipeline abgeschlossen ist, werden deren Daten hier automatisch angezeigt."
msgid "The time taken by each data entry gathered by that stage."
-msgstr "Zeit die für das jeweilige Ereignis in der Phase ermittelt wurde."
+msgstr "Zeit, die für das jeweilige Ereignis in der Phase ermittelt wurde."
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 "Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Median 5. Bei 3, 5, 7, 8 ist der Median (5+7)/2 = 6."
+msgid "There are problems accessing Git storage: "
+msgstr "Es gibt ein Problem beim Zugriff auf den Gitspeicher:"
+
+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."
+
msgid "Time before an issue gets scheduled"
-msgstr "Zeit bis ein Issue geplant wird"
+msgstr "Zeit bis ein Ticket geplant wird"
msgid "Time before an issue starts implementation"
-msgstr "Zeit bis die Implementierung für ein Issue beginnt"
+msgstr "Zeit bis die Implementierung für ein Ticket beginnt"
msgid "Time between merge request creation and merge/close"
-msgstr "Zeit zwischen Anlegen und Mergen/Schließen eines Merge Requests"
+msgstr "Zeit zwischen einem Merge Request und dessen Umsetzung / Schließung"
msgid "Time until first merge request"
msgstr "Zeit bis zum ersten Merge Request"
+msgid "Timeago|%s days ago"
+msgstr "seit %s Tagen"
+
+msgid "Timeago|%s days remaining"
+msgstr "%s Tage verbleibend"
+
+msgid "Timeago|%s hours remaining"
+msgstr "%s Stunden verbleibend"
+
+msgid "Timeago|%s minutes ago"
+msgstr "seit %s Minuten "
+
+msgid "Timeago|%s minutes remaining"
+msgstr "%s Minuten verbleibend"
+
+msgid "Timeago|%s months ago"
+msgstr "seit %s Monaten"
+
+msgid "Timeago|%s months remaining"
+msgstr "%s Monate verbleibend"
+
+msgid "Timeago|%s seconds remaining"
+msgstr "%s Sekunden verbleibend"
+
+msgid "Timeago|%s weeks ago"
+msgstr "seit %s Wochen"
+
+msgid "Timeago|%s weeks remaining"
+msgstr "%s Wochen verbleibend"
+
+msgid "Timeago|%s years ago"
+msgstr "seit %s Jahren"
+
+msgid "Timeago|%s years remaining"
+msgstr "%s Jahre verbleibend"
+
+msgid "Timeago|1 day remaining"
+msgstr "1 Tag verbleibend"
+
+msgid "Timeago|1 hour remaining"
+msgstr "1 Stunde verbleibend"
+
+msgid "Timeago|1 minute remaining"
+msgstr "1 Minute verbleibend"
+
+msgid "Timeago|1 month remaining"
+msgstr "1 Monat verbleibend"
+
+msgid "Timeago|1 week remaining"
+msgstr "1 Woche verbleibend"
+
+msgid "Timeago|1 year remaining"
+msgstr "1 Jahr verbleibend"
+
+msgid "Timeago|Past due"
+msgstr "Fällig"
+
+msgid "Timeago|a day ago"
+msgstr "vor einem Tag"
+
+msgid "Timeago|a month ago"
+msgstr "vor einem Monat"
+
+msgid "Timeago|a week ago"
+msgstr "vor einer Woche"
+
+msgid "Timeago|a while"
+msgstr "eine Weile"
+
+msgid "Timeago|a year ago"
+msgstr "vor einem Jahr"
+
+msgid "Timeago|about %s hours ago"
+msgstr "vor ungefähr %s Stunden"
+
+msgid "Timeago|about a minute ago"
+msgstr "vor ungefähr einer Minute"
+
+msgid "Timeago|about an hour ago"
+msgstr "vor ungefähr einer Stunde"
+
+msgid "Timeago|in %s days"
+msgstr "in %s Tagen"
+
+msgid "Timeago|in %s hours"
+msgstr "in %s Stunden"
+
+msgid "Timeago|in %s minutes"
+msgstr "in %s Minuten"
+
+msgid "Timeago|in %s months"
+msgstr "in %s Monaten"
+
+msgid "Timeago|in %s seconds"
+msgstr "in %s Sekunden"
+
+msgid "Timeago|in %s weeks"
+msgstr "in %s Wochen"
+
+msgid "Timeago|in %s years"
+msgstr "in %s Jahren"
+
+msgid "Timeago|in 1 day"
+msgstr "in 1 Tag"
+
+msgid "Timeago|in 1 hour"
+msgstr "in 1 Stunde"
+
+msgid "Timeago|in 1 minute"
+msgstr "in 1 Minute"
+
+msgid "Timeago|in 1 month"
+msgstr "in 1 Monat"
+
+msgid "Timeago|in 1 week"
+msgstr "in 1 Woche"
+
+msgid "Timeago|in 1 year"
+msgstr "in 1 Jahr"
+
+msgid "Timeago|less than a minute ago"
+msgstr "vor weniger als einer Minute"
+
msgid "Time|hr"
msgid_plural "Time|hrs"
-msgstr[0] "h"
-msgstr[1] "h"
+msgstr[0] "Std."
+msgstr[1] "Stdn."
msgid "Time|min"
msgid_plural "Time|mins"
-msgstr[0] "min"
-msgstr[1] "min"
+msgstr[0] "Min."
+msgstr[1] "Min."
msgid "Time|s"
-msgstr "s"
+msgstr "Sek."
msgid "Total Time"
msgstr "Gesamtzeit"
msgid "Total test time for all commits/merges"
-msgstr "Gesamte Testlaufzeit für alle Commits/Merges"
+msgstr "Gesamte Testzeit für alle Commits/Merges"
+
+msgid "Unstar"
+msgstr "Entfavorisieren"
+
+msgid "Upload New File"
+msgstr "Eine Neue Datei hochladen"
+
+msgid "Upload file"
+msgstr "Eine Datei hochladen"
+
+msgid "UploadLink|click to upload"
+msgstr "Zum Upload klicken"
+
+msgid "Use the following registration token during setup:"
+msgstr "Benutze den folgenden Registrierungstoken während des Setups:"
+
+msgid "Use your global notification setting"
+msgstr "Benutze Deine globalen Benachrichtigungseinstellungen"
+
+msgid "View open merge request"
+msgstr "Zeige offene Merge Requests."
+
+msgid "VisibilityLevel|Internal"
+msgstr "Intern"
+
+msgid "VisibilityLevel|Private"
+msgstr "Privat"
+
+msgid "VisibilityLevel|Public"
+msgstr "Öffentlich"
+
+msgid "VisibilityLevel|Unknown"
+msgstr "Unbekannt"
msgid "Want to see the data? Please ask an administrator for access."
-msgstr "Um diese Daten einsehen zu können, wenden Sie sich bitte an Ihren Administrator."
+msgstr "Du möchtest diese Daten sehen? Bitte frage einen Administrator nach dem Zugang."
msgid "We don't have enough data to show this stage."
msgstr "Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen."
-msgid "You have reached your project limit"
+msgid "Wiki"
msgstr ""
+msgid "Withdraw Access Request"
+msgstr "Zugriffsanfrage widerrufen"
+
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Du bist dabei %{group_name} zu entfernen. Entfernte Gruppen können NICHT wiederhergestellt werden! Bist Du dir WIRKLICH sicher?"
+
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Du bist dabei %{project_name_with_namespace} zu entfernen. Entfernte Projekte können NICHT wiederhergestellt werden! Bist Du dir WIRKLICH sicher?"
+
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr "Du bist dabei, die Beziehung des Ablegers zum Ursprungsprojekt %{forked_from_project}, zu entfernen. Bist Du dir WIRKLICH sicher?"
+
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr "Du bist dabei %{project_name_with_namespace} einem andere Besitzer zu übergeben. Bist Du dir WIRKLICH sicher?"
+
+msgid "You can only add files when you are on a branch"
+msgstr "Du kannst Dateien nur hinzufügen, wenn Du dich auf einem Branch befindest."
+
+msgid "You have reached your project limit"
+msgstr "Du hast die Projektbegrenzung erreicht."
+
+msgid "You must sign in to star a project"
+msgstr "Du musst angemeldet sein, um ein Projekt zu favorisieren."
+
msgid "You need permission."
-msgstr "Sie benötigen Zugriffsrechte."
+msgstr "Du brauchst eine Genehmigung."
+
+msgid "You will not get any notifications via email"
+msgstr "Du wirst keine Benachrichtigungen per E-Mail erhalten."
+
+msgid "You will only receive notifications for the events you choose"
+msgstr "Du wirst nur Benachrichtigungen für, von Dir ausgewählte, Ereignisse erhalten."
+
+msgid "You will only receive notifications for threads you have participated in"
+msgstr "Du wirst nur Benachrichtigungen für Unterhaltungen, an denen Du teilgenommen hast, erhalten."
+
+msgid "You will receive notifications for any activity"
+msgstr "Du wirst bei jeder Aktivität Benachrichtigungen erhalten."
+
+msgid "You will receive notifications only for comments in which you were @mentioned"
+msgstr "Du wirst nur Benachrichtigungen für Kommentare erhalten, in denen du @erwähnt wurdest."
+
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr "Du kannst erst mittels '%{protocol}' übertragen (push) oder abrufen (pull), nachdem Du für dein Konto '%{set_password_link}'."
+
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr "Du kannst erst mittels SSH übertragen (push) oder abrufen (pull), nachdem Du Deinem Konto '%{add_ssh_key_link}'."
+
+msgid "Your name"
+msgstr "Dein Name"
msgid "day"
msgid_plural "days"
msgstr[0] "Tag"
msgstr[1] "Tage"
+
+msgid "new merge request"
+msgstr "Neuer Merge Request"
+
+msgid "notification emails"
+msgstr "Benachrichtungsemail"
+
+msgid "parent"
+msgid_plural "parents"
+msgstr[0] "Vorgänger"
+msgstr[1] "Vorgänger"
+
diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po
index 46bf4e33997..0ac591d4927 100644
--- a/locale/en/gitlab.po
+++ b/locale/en/gitlab.po
@@ -17,17 +17,34 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"\n"
+msgid "%d commit"
+msgid_plural "%d commits"
+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] ""
msgstr[1] ""
-msgid "%d commit"
-msgid_plural "%d commits"
+msgid "%{commit_author_link} committed %{commit_timeago}"
+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 "%{commit_author_link} committed %{commit_timeago}"
+msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
msgid "1 pipeline"
@@ -41,6 +58,9 @@ msgstr ""
msgid "About auto deploy"
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 "Active"
msgstr ""
@@ -68,6 +88,18 @@ 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 "Attach a file by drag &amp; drop or %{upload_link}"
msgstr ""
@@ -109,6 +141,9 @@ msgstr ""
msgid "Cancel"
msgstr ""
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr ""
@@ -234,6 +269,9 @@ 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 ""
@@ -311,9 +349,15 @@ msgstr[1] ""
msgid "Description"
msgstr ""
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr ""
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr ""
@@ -397,12 +441,36 @@ msgstr ""
msgid "From merge request merge until deploy to production"
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 "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 "Home"
msgstr ""
@@ -412,6 +480,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr ""
@@ -470,6 +541,9 @@ msgstr ""
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr ""
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] ""
@@ -682,6 +756,9 @@ msgstr ""
msgid "Project access must be granted explicitly to each user."
msgstr ""
+msgid "Project details"
+msgstr ""
+
msgid "Project export could not be deleted."
msgstr ""
@@ -754,9 +831,21 @@ 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 ""
@@ -781,6 +870,9 @@ msgstr ""
msgid "Select a timezone"
msgstr ""
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr ""
@@ -807,12 +899,18 @@ msgstr[1] ""
msgid "Source code"
msgstr ""
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
msgid "StarProject|Star"
msgstr ""
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr ""
@@ -875,6 +973,9 @@ 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 means you can not push code until you create an empty repository or import existing one."
msgstr ""
@@ -1044,6 +1145,9 @@ msgstr ""
msgid "UploadLink|click to upload"
msgstr ""
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr ""
@@ -1059,6 +1163,9 @@ msgstr ""
msgid "VisibilityLevel|Public"
msgstr ""
+msgid "VisibilityLevel|Unknown"
+msgstr ""
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
@@ -1068,16 +1175,10 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
-msgid ""
-"You are going to remove %{group_name}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
+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}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
+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?"
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index d688478972d..8f25c893ecd 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -1,34 +1,51 @@
-# Huang Tao <htve@outlook.com>, 2017. #zanata
-# Lyubomir Vasilev <lyubomirv@abv.bg>, 2017. #zanata
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-13 12:07-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:21-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Esperanto\n"
+"Language: eo_UY\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-08-03 04:44-0400\n"
-"Last-Translator: Lyubomir Vasilev <lyubomirv@abv.bg>\n"
-"Language-Team: Esperanto (https://translate.zanata.org/project/view/GitLab)\n"
-"Language: eo\n"
-"X-Generator: Zanata 3.9.6\n"
-"Plural-Forms: nplurals=2; plural=(n != 1)\n"
-
-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 enmetado estis transsaltita, por ne troÅarÄi la sistemon."
-msgstr[1] "%s enmetadoj estis transsaltitaj, por ne troÅarÄi la sistemon."
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: eo\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d enmetado"
msgstr[1] "%d enmetadoj"
+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 enmetado estis transsaltita, por ne troÅarÄi la sistemon."
+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_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 ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 ĉenstablo"
@@ -40,6 +57,18 @@ msgstr "Aro da diagramoj pri la seninterrompa integrado"
msgid "About auto deploy"
msgstr "Pri la aÅ­tomata disponigado"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr "Aktiva"
@@ -56,35 +85,105 @@ msgid "Add License"
msgstr "Aldoni rajtigilon"
msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr ""
-"Aldonu SSH-Ålosilon al via profilo por ebligi al vi eltiri kaj alpuÅi per "
-"SSH."
+msgstr "Aldonu SSH-Ålosilon al via profilo por ebligi al vi eltiri kaj alpuÅi per SSH."
msgid "Add new directory"
msgstr "Aldoni novan dosierujon"
+msgid "All"
+msgstr ""
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "Arkivita projekto! La deponejo permesas nur legadon"
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Ĉu vi certe volas forigi ĉi tiun ĉenstablan planon?"
+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 "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Alkroĉu dosieron per Åovmetado aÅ­ %{upload_link}"
+msgid "Authentication log"
+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 "Branch"
msgid_plural "Branches"
msgstr[0] "Branĉo"
msgstr[1] "Branĉoj"
-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 branĉo <strong>%{branch_name}</strong> estis kreita. Por agordi aŭtomatan "
-"disponigadon, bonvolu elekti Yaml-Åablonon por GitLab CI kaj enmeti viajn "
-"ÅanÄojn. %{link_to_autodeploy_doc}"
+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 branĉo <strong>%{branch_name}</strong> estis kreita. Por agordi aÅ­tomatan disponigadon, bonvolu elekti Yaml-Åablonon por GitLab CI kaj enmeti viajn ÅanÄojn. %{link_to_autodeploy_doc}"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Serĉu branĉon"
@@ -110,12 +209,18 @@ msgstr "Elekti dosierojn"
msgid "ByAuthor|by"
msgstr "de"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "Agordoj de seninterrompa integrado"
msgid "Cancel"
msgstr "Nuligi"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Elekti en branĉon"
@@ -134,6 +239,9 @@ msgstr "Listo de ÅanÄoj"
msgid "Charts"
msgstr "Diagramoj"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Precize elekti ĉi tiun kunmetadon"
@@ -194,6 +302,9 @@ msgstr "transsaltita"
msgid "CiStatus|running"
msgstr "plenumiÄanta"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "Enmetado"
@@ -226,12 +337,18 @@ msgstr "Enmetita de"
msgid "Compare"
msgstr "Kompari"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "Gvidlinioj por kontribuado"
msgid "Contributors"
msgstr "Kontribuantoj"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "Kopii la adreson en la kopibufron"
@@ -241,12 +358,11 @@ msgstr "Kopii la identigilon de la enmetado"
msgid "Create New Directory"
msgstr "Krei novan dosierujon"
-msgid ""
-"Create a personal access token on your account to pull or push via "
-"%{protocol}."
+msgid "Create a new branch"
msgstr ""
-"Kreu propran atingoĵetonon en via konto por ebligi al vi eltiri kaj alpuÅi "
-"per %{protocol}."
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr "Kreu propran atingoĵetonon en via konto por ebligi al vi eltiri kaj alpuÅi per %{protocol}."
msgid "Create directory"
msgstr "Krei dosierujon"
@@ -278,24 +394,14 @@ msgstr "La sintakso de Cron"
msgid "Custom notification events"
msgstr "Propraj sciigaj eventoj"
-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 ""
-"La propraj sciigaj niveloj estas la samaj kiel la niveloj de partoprenado. "
-"Uzante la proprajn sciigajn nivelojn, vi ricevos ankaÅ­ sciigojn por "
-"elektitaj de vi eventoj. Por lerni pli, bonvolu vidi %{notification_link}."
+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 "La propraj sciigaj niveloj estas la samaj kiel la niveloj de partoprenado. Uzante la proprajn sciigajn nivelojn, vi ricevos ankaÅ­ sciigojn por elektitaj de vi eventoj. Por lerni pli, bonvolu vidi %{notification_link}."
msgid "Cycle Analytics"
msgstr "Cikla analizo"
-msgid ""
-"Cycle Analytics gives an overview of how much time it takes to go from idea "
-"to production in your project."
-msgstr ""
-"La cikla analizo esploras kiom da tempo necesas por disvolvi ideon Äis Äi "
-"fariÄos realaĵo."
+msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
+msgstr "La cikla analizo esploras kiom da tempo necesas por disvolvi ideon Äis Äi fariÄos realaĵo."
msgid "CycleAnalyticsStage|Code"
msgstr "Programado"
@@ -329,12 +435,21 @@ msgid_plural "Deploys"
msgstr[0] "Disponigado"
msgstr[1] "Disponigadoj"
+msgid "Deploy Keys"
+msgstr ""
+
msgid "Description"
msgstr "Priskribo"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "Nomo de dosierujo"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "Ne montru denove"
@@ -371,6 +486,27 @@ msgstr "Redakti"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Redakti ĉenstablan planon %{id}"
+msgid "Emails"
+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 "Ĉiutage (je 4:00)"
@@ -416,9 +552,19 @@ msgid "From issue creation until deploy to production"
msgstr "De la kreado de la problemo Äis la disponigado en la publika versio"
msgid "From merge request merge until deploy to production"
+msgstr "De la kunfandado de la peto pri kunfando Äis la disponigado en la publika versio"
+
+msgid "GPG Keys"
+msgstr ""
+
+msgid "Geo Nodes"
+msgstr ""
+
+msgid "Git storage health information has been reset"
+msgstr ""
+
+msgid "GitLab Runner section"
msgstr ""
-"De la kunfandado de la peto pri kunfando Äis la disponigado en la publika "
-"versio"
msgid "Go to your fork"
msgstr "Al via disbranĉigo"
@@ -426,29 +572,53 @@ msgstr "Al via disbranĉigo"
msgid "GoToYourFork|Fork"
msgstr "Disbranĉigo"
+msgid "Group overview"
+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 "Home"
msgstr "Hejmo"
+msgid "Hooks"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "La refreÅigo komenciÄis sukcese"
msgid "Import repository"
msgstr "Enporti deponejon"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Intervala Åablono"
msgid "Introducing Cycle Analytics"
msgstr "Ni prezentas al vi la ciklan analizon"
-msgid "Jobs for last month"
-msgstr "Taskoj po la lasta monato"
-
-msgid "Jobs for last week"
-msgstr "Taskoj po la lasta semajno"
+msgid "Issue events"
+msgstr ""
-msgid "Jobs for last year"
-msgstr "Taskoj po la lasta jaro"
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "MalÅaltita"
@@ -456,6 +626,9 @@ msgstr "MalÅaltita"
msgid "LFSStatus|Enabled"
msgstr "Åœaltita"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "La lasta %d tago"
@@ -470,6 +643,12 @@ msgstr "Lasta Äisdatigo"
msgid "Last commit"
msgstr "Lasta enmetado"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "Lernu pli en la"
@@ -482,17 +661,41 @@ msgstr "Forlasi la grupon"
msgid "Leave project"
msgstr "Forlasi la projekton"
+msgid "License"
+msgstr ""
+
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "Limigita al montrado de ne pli ol %d evento"
msgstr[1] "Limigita al montrado de ne pli ol %d eventoj"
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "Mediano"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "aldonos SSH-Ålosilon"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nova problemo"
@@ -591,6 +794,9 @@ msgstr "Partoprenado"
msgid "NotificationLevel|Watch"
msgstr "Rigardado"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrilo"
@@ -600,9 +806,15 @@ msgstr "Malfermita"
msgid "Options"
msgstr "Opcioj"
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
msgstr "Posedanto"
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
msgstr "Ĉenstablo"
@@ -615,6 +827,9 @@ msgstr "Ĉenstabla plano"
msgid "Pipeline Schedules"
msgstr "Ĉenstablaj planoj"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Malsukcesaj:"
@@ -678,6 +893,15 @@ msgstr "Ĉenstabloj"
msgid "Pipelines charts"
msgstr "Ĉenstablaj diagramoj"
+msgid "Pipelines for last month"
+msgstr ""
+
+msgid "Pipelines for last week"
+msgstr ""
+
+msgid "Pipelines for last year"
+msgstr ""
+
msgid "Pipeline|all"
msgstr "ĉiuj"
@@ -690,6 +914,15 @@ msgstr "kun etapo"
msgid "Pipeline|with stages"
msgstr "kun etapoj"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "La projekto „%{project_name}“ estis alvicigita por forigado."
@@ -705,27 +938,30 @@ msgstr "La projekto „%{project_name}“ estos forigita."
msgid "Project access must be granted explicitly to each user."
msgstr "Ĉiu uzanto devas akiri propran atingon al la projekto."
+msgid "Project details"
+msgstr ""
+
msgid "Project export could not be deleted."
msgstr "Ne eblas forigi la projektan elporton."
msgid "Project export has been deleted."
msgstr "La projekta elporto estis forigita."
-msgid ""
-"Project export link has expired. Please generate a new export from your "
-"project settings."
-msgstr ""
-"La ligilo por la projekta elporto eksvalidiÄis. Bonvolu krei novan elporton "
-"en la agordoj de la projekto."
+msgid "Project export link has expired. Please generate a new export from your project settings."
+msgstr "La ligilo por la projekta elporto eksvalidiÄis. Bonvolu krei novan elporton en la agordoj de la projekto."
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."
+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 ""
+
msgid "ProjectFeature|Disabled"
msgstr "MalÅaltita"
@@ -747,6 +983,12 @@ msgstr "Etapo"
msgid "ProjectNetworkGraph|Graph"
msgstr "Grafeo"
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Legu pli"
@@ -783,15 +1025,30 @@ msgstr "Rememorigu denove"
msgid "Remove project"
msgstr "Forigi la projekton"
+msgid "Repository"
+msgstr ""
+
msgid "Request Access"
msgstr "Peti atingeblon"
+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 "Malfari ĉi tiun enmetadon"
msgid "Revert this merge request"
msgstr "Malfari ĉi tiun peton pri kunfando"
+msgid "SSH Keys"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "Konservi ĉenstablan planon"
@@ -810,13 +1067,17 @@ msgstr "Elektu formaton de arkivo"
msgid "Select a timezone"
msgstr "Elektu horzonon"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "Elektu celan branĉon"
-msgid "Set a password on your account to pull or push via %{protocol}."
+msgid "Service Templates"
msgstr ""
-"Kreu pasvorton por via konto por ebligi al vi eltiri kaj alpuÅi per "
-"%{protocol}."
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr "Kreu pasvorton por via konto por ebligi al vi eltiri kaj alpuÅi per %{protocol}."
msgid "Set up CI"
msgstr "Agordi SI"
@@ -830,20 +1091,35 @@ msgstr "Agordi aÅ­tomatan disponigadon"
msgid "SetPasswordToCloneLink|set a password"
msgstr "kreos pasvorton"
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Estas montrata %d evento"
msgstr[1] "Estas montrataj %d eventoj"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "Kodo"
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
msgid "StarProject|Star"
msgstr "Steligi"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Kreu %{new_merge_request} kun ĉi tiuj ÅanÄoj"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Iri al branĉo/etikedo"
@@ -858,62 +1134,32 @@ msgstr "Etikedoj"
msgid "Target Branch"
msgstr "Cela branĉo"
-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."
+msgid "Team"
msgstr ""
-"La etapo de programado montras la tempon de la unua enmetado Äis la kreado "
-"de la peto pri kunfando. La datenoj aldoniÄos aÅ­tomate ĉi tie post kiam vi "
-"kreas la unuan peton pri kunfando."
+
+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 "La etapo de programado montras la tempon de la unua enmetado Äis la kreado de la peto pri kunfando. La datenoj aldoniÄos aÅ­tomate ĉi tie post kiam vi kreas la unuan peton pri kunfando."
msgid "The collection of events added to the data gathered for that stage."
-msgstr ""
-"La aro da eventoj, kiuj estas aldonitaj al la datenoj kolektitaj por la "
-"etapo."
+msgstr "La aro da eventoj, kiuj estas aldonitaj al la datenoj kolektitaj por la etapo."
msgid "The fork relationship has been removed."
msgstr "La rilato de disbranĉigo estis forigita."
-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 ""
-"La etapo de la problemo montras kiom la tempo pasas de la kreado de problemo "
-"Äis la atribuado de la problemo al cela etapo de la projekto, aÅ­ al listo "
-"sur la problemtabulo. Komencu krei problemojn por vidi la datenojn por ĉi "
-"tiu etapo."
+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 "La etapo de la problemo montras kiom la tempo pasas de la kreado de problemo Äis la atribuado de la problemo al cela etapo de la projekto, aÅ­ al listo sur la problemtabulo. Komencu krei problemojn por vidi la datenojn por ĉi tiu etapo."
msgid "The phase of the development lifecycle."
msgstr "La etapo de la disvolva ciklo."
-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 ""
-"La ĉenstabla plano plenumas ĉenstablojn en la estonteco, ripete, por "
-"difinitaj branĉoj aŭ etikedoj. Tiuj planitaj ĉenstabloj heredos la limigitan "
-"atingon al la projekto de la rilata uzanto."
+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 "La ĉenstabla plano plenumas ĉenstablojn en la estonteco, ripete, por difinitaj branĉoj aŭ etikedoj. Tiuj planitaj ĉenstabloj heredos la limigitan atingon al la projekto de la rilata uzanto."
-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 ""
-"La etapo de la plano montras la tempon de la antaÅ­a Åtupo Äis la alpuÅado de "
-"via unua enmetado. Ĉi tiu tempo aldoniÄos aÅ­tomate post kiam vi alpuÅas la "
-"unuan enmetadon."
+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 "La etapo de la plano montras la tempon de la antaÅ­a Åtupo Äis la alpuÅado de via unua enmetado. Ĉi tiu tempo aldoniÄos aÅ­tomate post kiam vi alpuÅas la unuan enmetadon."
-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 ""
-"La etapo de eldonado montras la tutan tempon de la kreado de problemo Äis la "
-"disponigado en la publika versio. La datenoj aldoniÄos aÅ­tomate post kiam vi "
-"kompletigos plenan ciklon de ideo Äis realaĵo."
+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 "La etapo de eldonado montras la tutan tempon de la kreado de problemo Äis la disponigado en la publika versio. La datenoj aldoniÄos aÅ­tomate post kiam vi kompletigos plenan ciklon de ideo Äis realaĵo."
msgid "The project can be accessed by any logged in user."
msgstr "Ĉiu ensalutita uzanto havas atingon al la projekto"
@@ -924,52 +1170,26 @@ msgstr "Ĉiu povas havi atingon al la projekto, sen ensaluti"
msgid "The repository for this project does not exist."
msgstr "La deponejo por ĉi tiu projekto ne ekzistas."
-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 ""
-"La etapo de la kontrolo montras la tempon de la kreado de la peto pri "
-"kunfando Äis Äia aplikado. La datenoj aldoniÄos aÅ­tomate post kiam vi "
-"aplikos la unuan peton pri kunfando."
+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 "La etapo de la kontrolo montras la tempon de la kreado de la peto pri kunfando Äis Äia aplikado. La datenoj aldoniÄos aÅ­tomate post kiam vi aplikos la unuan peton pri kunfando."
-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 ""
-"La etapo de preparo por eldono montras la tempon inter la aplikado de la "
-"peto pri kunfando kaj la disponigado de la kodo en la publika versio. La "
-"datenoj aldoniÄos aÅ­tomate post kiam vi faros la unuan disponigadon en la "
-"publika versio."
+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 "La etapo de preparo por eldono montras la tempon inter la aplikado de la peto pri kunfando kaj la disponigado de la kodo en la publika versio. La datenoj aldoniÄos aÅ­tomate post kiam vi faros la unuan disponigadon en la publika versio."
-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 ""
-"La etapo de testado montras kiom da tempo necesas al „GitLab CI“ por plenumi "
-"ĉiujn ĉenstablojn por la rilata peto pri kunfando. La datenoj aldoniÄos "
-"aÅ­tomate post kiam via unua ĉenstablo finiÄos."
+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 "La etapo de testado montras kiom da tempo necesas al „GitLab CI“ por plenumi ĉiujn ĉenstablojn por la rilata peto pri kunfando. La datenoj aldoniÄos aÅ­tomate post kiam via unua ĉenstablo finiÄos."
msgid "The time taken by each data entry gathered by that stage."
msgstr "La tempo, kiu estas necesa por ĉiu dateno kolektita de la etapo."
-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 ""
-"La valoro, kiu troviÄas en la mezo de aro da rigardataj valoroj. Ekzemple: "
-"inter 3, 5 kaj 9, la mediano estas 5. Inter 3, 5, 7 kaj 8, la mediano estas "
-"(5+7)/2 = 6."
+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 "La valoro, kiu troviÄas en la mezo de aro da rigardataj valoroj. Ekzemple: inter 3, 5 kaj 9, la mediano estas 5. Inter 3, 5, 7 kaj 8, la mediano estas (5+7)/2 = 6."
-msgid ""
-"This means you can not push code until you create an empty repository or "
-"import existing one."
+msgid "There are problems accessing Git storage: "
msgstr ""
-"Ĉi tiu signifas, ke vi ne povos alpuÅi kodon, antaÅ­ ol vi kreos malplenan "
-"deponejon aÅ­ enportos jam ekzistantan."
+
+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."
msgid "Time before an issue gets scheduled"
msgstr "Tempo antaÅ­ problemo estas planita por ellabori"
@@ -1137,6 +1357,9 @@ msgstr "AlÅuti dosieron"
msgid "UploadLink|click to upload"
msgstr "alklaku por alÅuti"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "Uzi vian Äeneralan agordon pri la sciigoj"
@@ -1156,46 +1379,28 @@ msgid "VisibilityLevel|Unknown"
msgstr "Nekonata"
msgid "Want to see the data? Please ask an administrator for access."
-msgstr ""
-"Ĉu vi volas vidi la datenojn? Bonvolu peti atingeblon de administranto."
+msgstr "Ĉu vi volas vidi la datenojn? Bonvolu peti atingeblon de administranto."
msgid "We don't have enough data to show this stage."
msgstr "Ne estas sufiĉe da datenoj por montri ĉi tiun etapon."
+msgid "Wiki"
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr "Nuligi la peton pri atingeblo"
-msgid ""
-"You are going to remove %{group_name}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Vi forigos „%{group_name}“.\n"
-"Oni NE POVAS malfari la forigon de grupo!\n"
-"Ĉu vi estas ABSOLUTE certa?"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Vi forigos „%{group_name}“. Oni NE POVAS malfari la forigon de grupo! Ĉu vi estas ABSOLUTE certa?"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Vi forigos „%{project_name_with_namespace}“.\n"
-"Oni NE POVAS malfari la forigon de projekto!\n"
-"Ĉu vi estas ABSOLUTE certa?"
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Vi forigos „%{project_name_with_namespace}“. Oni NE POVAS malfari la forigon de projekto! Ĉu vi estas ABSOLUTE certa?"
-msgid ""
-"You are going to remove the fork relationship to source project "
-"%{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr ""
-"Vi forigos la rilaton de la disbranĉigo al la originala projekto, "
-"„%{forked_from_project}“. Ĉu vi estas ABSOLUTE certa?"
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr "Vi forigos la rilaton de la disbranĉigo al la originala projekto, „%{forked_from_project}“. Ĉu vi estas ABSOLUTE certa?"
-msgid ""
-"You are going to transfer %{project_name_with_namespace} to another owner. "
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Vi transigos „%{project_name_with_namespace}“ al alia posedanto. Ĉu vi estas "
-"ABSOLUTE certa?"
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr "Vi transigos „%{project_name_with_namespace}“ al alia posedanto. Ĉu vi estas ABSOLUTE certa?"
msgid "You can only add files when you are on a branch"
msgstr "Oni povas aldoni dosierojn nur kiam oni estas en branĉo"
@@ -1215,31 +1420,20 @@ msgstr "VI ne ricevos sciigojn per retpoÅto"
msgid "You will only receive notifications for the events you choose"
msgstr "Vi ricevos sciigojn nur por la eventoj elektitaj de vi"
-msgid ""
-"You will only receive notifications for threads you have participated in"
+msgid "You will only receive notifications for threads you have participated in"
msgstr "Vi ricevos sciigojn nur por la fadenoj, en kiuj vi partoprenis"
msgid "You will receive notifications for any activity"
msgstr "Vi ricevos sciigojn por ĉiu ago"
-msgid ""
-"You will receive notifications only for comments in which you were "
-"@mentioned"
+msgid "You will receive notifications only for comments in which you were @mentioned"
msgstr "Vi ricevos sciigojn nur por komentoj, en kiuj vi estas @menciita"
-msgid ""
-"You won't be able to pull or push project code via %{protocol} until you "
-"%{set_password_link} on your account"
-msgstr ""
-"Vi ne povos eltiri aÅ­ alpuÅi kodon per %{protocol} antaÅ­ ol vi "
-"%{set_password_link} por via konto"
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr "Vi ne povos eltiri aÅ­ alpuÅi kodon per %{protocol} antaÅ­ ol vi %{set_password_link} por via konto"
-msgid ""
-"You won't be able to pull or push project code via SSH until you "
-"%{add_ssh_key_link} to your profile"
-msgstr ""
-"Vi ne povos eltiri aÅ­ alpuÅi kodon per SSH antaÅ­ ol vi %{add_ssh_key_link} "
-"al via profilo"
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr "Vi ne povos eltiri aÅ­ alpuÅi kodon per SSH antaÅ­ ol vi %{add_ssh_key_link} al via profilo"
msgid "Your name"
msgstr "Via nomo"
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index 5c669d51a68..eee720d5ba2 100644
--- a/locale/es/gitlab.po
+++ b/locale/es/gitlab.po
@@ -1,21 +1,20 @@
-# Spanish translations for gitlab package.
-# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the gitlab package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
-#
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"PO-Revision-Date: 2017-07-13 12:10-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Spanish\n"
-"Language: es\n"
+"Language: es_ES\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"
-"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
-"X-Generator: Poedit 2.0.2\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: es-ES\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
msgid "%d commit"
msgid_plural "%d commits"
@@ -30,10 +29,27 @@ 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_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 ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "1 pipeline"
-msgstr[1] "%d pipelines"
+msgstr[0] ""
+msgstr[1] ""
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Una colección de gráficos sobre Integración Continua"
@@ -41,6 +57,18 @@ msgstr "Una colección de gráficos sobre Integración Continua"
msgid "About auto deploy"
msgstr "Acerca del auto despliegue"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr "Activo"
@@ -62,15 +90,93 @@ msgstr "Agregar una clave SSH a tu perfil para actualizar o enviar a través de
msgid "Add new directory"
msgstr "Agregar nuevo directorio"
+msgid "All"
+msgstr ""
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "¡Proyecto archivado! El repositorio es de solo lectura"
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "¿Estás seguro que deseas eliminar esta programación del pipeline?"
+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 "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Adjunte un archivo arrastrando &amp; soltando o %{upload_link}"
+msgid "Authentication log"
+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 "Branch"
msgid_plural "Branches"
msgstr[0] "Rama"
@@ -103,12 +209,18 @@ msgstr "Examinar archivos"
msgid "ByAuthor|by"
msgstr "por"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "Configuración de CI"
msgid "Cancel"
msgstr "Cancelar"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Escoger en la rama"
@@ -122,11 +234,14 @@ msgid "ChangeTypeAction|Revert"
msgstr "Revertir"
msgid "Changelog"
-msgstr "Changelog"
+msgstr ""
msgid "Charts"
msgstr "Gráficos"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Escoger este cambio"
@@ -187,6 +302,9 @@ msgstr "omitido"
msgid "CiStatus|running"
msgstr "en ejecución"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "Cambio"
@@ -219,12 +337,18 @@ msgstr "Enviado por"
msgid "Compare"
msgstr "Comparar"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "Guía de contribución"
msgid "Contributors"
msgstr "Contribuidores"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "Copiar URL al portapapeles"
@@ -234,6 +358,9 @@ msgstr "Copiar SHA del cambio al portapapeles"
msgid "Create New Directory"
msgstr "Crear Nuevo Directorio"
+msgid "Create a new branch"
+msgstr ""
+
msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "Crear un token de acceso personal en tu cuenta para actualizar o enviar a través de %{protocol}."
@@ -271,7 +398,7 @@ msgid "Custom notification levels are the same as participating levels. With cus
msgstr "Los niveles de notificación personalizados son los mismos que los niveles participantes. Con los niveles de notificación personalizados, también recibirá notificaciones para eventos seleccionados. Para obtener más información, consulte %{notification_link}."
msgid "Cycle Analytics"
-msgstr "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 "Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."
@@ -308,12 +435,21 @@ msgid_plural "Deploys"
msgstr[0] "Despliegue"
msgstr[1] "Despliegues"
+msgid "Deploy Keys"
+msgstr ""
+
msgid "Description"
msgstr "Descripción"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "Nombre del directorio"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "No mostrar de nuevo"
@@ -350,6 +486,27 @@ msgstr "Editar"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Editar Programación del Pipeline %{id}"
+msgid "Emails"
+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 "Todos los días (a las 4:00 am)"
@@ -397,35 +554,71 @@ msgstr "Desde la creación de la incidencia hasta el despliegue a producción"
msgid "From merge request merge until deploy to production"
msgstr "Desde la integración de la solicitud de fusión hasta el despliegue a producción"
+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 "Ir a tu bifurcación"
msgid "GoToYourFork|Fork"
msgstr "Bifurcación"
+msgid "Group overview"
+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 "Home"
msgstr "Inicio"
+msgid "Hooks"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "Servicio de limpieza iniciado con éxito"
msgid "Import repository"
msgstr "Importar repositorio"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Patrón de intervalo"
msgid "Introducing Cycle Analytics"
msgstr "Introducción a Cycle Analytics"
-msgid "Jobs for last month"
-msgstr "Trabajos del mes pasado"
-
-msgid "Jobs for last week"
-msgstr "Trabajos de la semana pasada"
+msgid "Issue events"
+msgstr ""
-msgid "Jobs for last year"
-msgstr "Trabajos del año pasado"
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "Deshabilitado"
@@ -433,6 +626,9 @@ msgstr "Deshabilitado"
msgid "LFSStatus|Enabled"
msgstr "Habilitado"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "Último %d día"
@@ -447,6 +643,12 @@ msgstr "Última actualización"
msgid "Last commit"
msgstr "Último cambio"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "Más información en la"
@@ -459,17 +661,41 @@ msgstr "Abandonar grupo"
msgid "Leave project"
msgstr "Abandonar proyecto"
+msgid "License"
+msgstr ""
+
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "Limitado a mostrar máximo %d evento"
msgstr[1] "Limitado a mostrar máximo %d eventos"
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "Mediana"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "agregar una clave SSH"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nueva incidencia"
@@ -568,6 +794,9 @@ msgstr "Participación"
msgid "NotificationLevel|Watch"
msgstr "Vigilancia"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrar"
@@ -577,11 +806,17 @@ msgstr "Abierto"
msgid "Options"
msgstr "Opciones"
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
msgstr "Propietario"
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
-msgstr "Pipeline"
+msgstr ""
msgid "Pipeline Health"
msgstr "Estado del Pipeline"
@@ -592,6 +827,9 @@ msgstr "Programación del Pipeline"
msgid "Pipeline Schedules"
msgstr "Programaciones de los Pipelines"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Fallidos:"
@@ -650,11 +888,20 @@ msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "Personalizado"
msgid "Pipelines"
-msgstr "Pipelines"
+msgstr ""
msgid "Pipelines charts"
msgstr "Gráficos de los pipelines"
+msgid "Pipelines for last month"
+msgstr ""
+
+msgid "Pipelines for last week"
+msgstr ""
+
+msgid "Pipelines for last year"
+msgstr ""
+
msgid "Pipeline|all"
msgstr "todos"
@@ -667,6 +914,15 @@ msgstr "con etapa"
msgid "Pipeline|with stages"
msgstr "con etapas"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Proyecto ‘%{project_name}’ en cola para eliminación."
@@ -682,6 +938,9 @@ msgstr "Proyecto ‘%{project_name}’ será eliminado."
msgid "Project access must be granted explicitly to each user."
msgstr "El acceso al proyecto debe concederse explícitamente a cada usuario."
+msgid "Project details"
+msgstr ""
+
msgid "Project export could not be deleted."
msgstr "No se pudo eliminar la exportación del proyecto."
@@ -697,6 +956,12 @@ msgstr "Se inició la exportación del proyecto. Se enviará un enlace de descar
msgid "Project home"
msgstr "Inicio del proyecto"
+msgid "Project overview"
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "Deshabilitada"
@@ -718,6 +983,12 @@ msgstr "Etapa"
msgid "ProjectNetworkGraph|Graph"
msgstr "Historial gráfico"
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Leer más"
@@ -754,15 +1025,30 @@ msgstr "Recordar después"
msgid "Remove project"
msgstr "Eliminar proyecto"
+msgid "Repository"
+msgstr ""
+
msgid "Request Access"
msgstr "Solicitar acceso"
+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 "Revertir este cambio"
msgid "Revert this merge request"
msgstr "Revertir esta solicitud de fusión"
+msgid "SSH Keys"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "Guardar programación del pipeline"
@@ -781,9 +1067,15 @@ msgstr "Seleccionar formato de archivo"
msgid "Select a timezone"
msgstr "Selecciona una zona horaria"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "Selecciona una rama de destino"
+msgid "Service Templates"
+msgstr ""
+
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "Establezca una contraseña en su cuenta para actualizar o enviar a través de %{protocol}."
@@ -799,20 +1091,35 @@ msgstr "Configurar auto despliegue"
msgid "SetPasswordToCloneLink|set a password"
msgstr "establecer una contraseña"
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Mostrando %d evento"
msgstr[1] "Mostrando %d eventos"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "Código fuente"
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
msgid "StarProject|Star"
msgstr "Destacar"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Iniciar una %{new_merge_request} con estos cambios"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Cambiar rama/etiqueta"
@@ -827,6 +1134,9 @@ msgstr "Etiquetas"
msgid "Target Branch"
msgstr "Rama de destino"
+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 "La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."
@@ -875,6 +1185,9 @@ msgstr "El tiempo utilizado por cada entrada de datos obtenido por esa etapa."
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 "El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."
+msgid "There are problems accessing Git storage: "
+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."
@@ -1044,6 +1357,9 @@ msgstr "Subir archivo"
msgid "UploadLink|click to upload"
msgstr "Hacer clic para subir"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "Utiliza tu configuración de notificación global"
@@ -1068,26 +1384,17 @@ msgstr "¿Quieres ver los datos? Por favor pide acceso al administrador."
msgid "We don't have enough data to show this stage."
msgstr "No hay suficientes datos para mostrar en esta etapa."
+msgid "Wiki"
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr "Retirar Solicitud de Acceso"
-msgid ""
-"You are going to remove %{group_name}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Va a eliminar %{group_name}.\n"
-"¡El grupo eliminado NO puede ser restaurado!\n"
-"¿Estás TOTALMENTE seguro?"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Va a eliminar %{group_name}. ¡El grupo eliminado NO puede ser restaurado! ¿Estás TOTALMENTE seguro?"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Va a eliminar %{project_name_with_namespace}.\n"
-"¡El proyecto eliminado NO puede ser restaurado!\n"
-"¿Estás TOTALMENTE seguro?"
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Va a eliminar %{project_name_with_namespace}. ¡El proyecto eliminado NO puede ser restaurado! ¿Estás TOTALMENTE seguro?"
msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
msgstr "Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?"
@@ -1146,3 +1453,4 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "padre"
msgstr[1] "padres"
+
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index c490933c6d4..43e66d8dea4 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -1,43 +1,55 @@
-# Dremor <egeorget@opmbx.org>, 2017. #zanata
-# Huang Tao <htve@outlook.com>, 2017. #zanata
-# Rémy Coutable <remy@rymai.me>, 2017. #zanata
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-13 12:07-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: French\n"
+"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Language-Team: French (https://translate.zanata.org/project/view/GitLab)\n"
-"PO-Revision-Date: 2017-08-03 03:35-0400\n"
-"Last-Translator: Rémy Coutable <remy@rymai.me>\n"
-"Language: fr\n"
-"X-Generator: Zanata 3.9.6\n"
-"Plural-Forms: nplurals=2; plural=(n > 1)\n"
-
-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 validation supplémentaire a été masquée afin d'éviter de créer de "
-"problèmes de performances."
-msgstr[1] ""
-"%s validations supplémentaires ont été masquées afin d'éviter de créer de "
-"problèmes de performances."
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: fr\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d validation"
msgstr[1] "%d validations"
+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 validation supplémentaire a été masquée afin d'éviter de créer de problèmes de performances."
+msgstr[1] "%s validations supplémentaires ont été masquées afin d'éviter de créer de problèmes de performances."
+
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."
+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 ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "1 pipeline"
-msgstr[1] "%d pipelines"
+msgstr[0] ""
+msgstr[1] ""
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Un ensemble de graphiques concernant l’Intégration Continue (CI)"
@@ -45,6 +57,18 @@ msgstr "Un ensemble de graphiques concernant l’Intégration Continue (CI)"
msgid "About auto deploy"
msgstr "A propos de l'auto-déploiement"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr "Actif"
@@ -61,36 +85,105 @@ msgid "Add License"
msgstr "Ajouter une licence"
msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr ""
-"Ajoutez une clef SSH à votre profil pour pouvoir récupérer et pousser par "
-"SSH."
+msgstr "Ajoutez une clef SSH à votre profil pour pouvoir récupérer et pousser par SSH."
msgid "Add new directory"
msgstr "Ajouter un nouveau dossier"
+msgid "All"
+msgstr ""
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "Projet archivé ! Le dépôt est en lecture seule"
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 ""
+
+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 "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"
+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 "Branch"
msgid_plural "Branches"
-msgstr[0] "Branche"
-msgstr[1] "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 ""
-"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}"
+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}"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Rechercher la branche"
@@ -99,7 +192,7 @@ msgid "BranchSwitcherTitle|Switch branch"
msgstr "Changer de branche"
msgid "Branches"
-msgstr "Branches"
+msgstr ""
msgid "Browse Directory"
msgstr "Parcourir le dossier"
@@ -116,12 +209,18 @@ msgstr "Parcourir les fichiers"
msgid "ByAuthor|by"
msgstr "par"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "Configuration de l'intégration continue (CI)"
msgid "Cancel"
msgstr "Annuler"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Sélectionner dans la branche"
@@ -140,6 +239,9 @@ msgstr "Journal des modifications"
msgid "Charts"
msgstr "Graphiques"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Sélectionner cette validation"
@@ -200,6 +302,9 @@ msgstr "ignoré"
msgid "CiStatus|running"
msgstr "en cours"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "Validation"
@@ -232,12 +337,18 @@ msgstr "Validé par"
msgid "Compare"
msgstr "Comparer"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "Guilde de contribution"
msgid "Contributors"
msgstr "Contributeurs"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "Copier l'URL dans le presse-papier"
@@ -247,12 +358,11 @@ msgstr "Copier le SHA de la validation"
msgid "Create New Directory"
msgstr "Créer un nouveau dossier"
-msgid ""
-"Create a personal access token on your account to pull or push via "
-"%{protocol}."
+msgid "Create a new branch"
msgstr ""
-"Créer un jeton d’accès personnel pour votre compte afin de récupérer ou "
-"pousser par %{protocol}."
+
+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}."
msgid "Create directory"
msgstr "Créer un dossier"
@@ -284,25 +394,14 @@ msgstr "Syntaxe Cron"
msgid "Custom notification events"
msgstr "Événements de notification personnalisés"
-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 ""
-"Le niveau de notification Personnalisé est similaire au niveau Participation."
-" Cependant, il permet également de recevoir des notifications pour des "
-"événements sélectionnés. Pour plus d’information, vous pouvez consulter "
-"%{notification_link}."
+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 "Le niveau de notification Personnalisé est similaire au niveau Participation. Cependant, il permet également de recevoir des notifications pour des événements sélectionnés. Pour plus d’information, vous pouvez consulter %{notification_link}."
msgid "Cycle Analytics"
msgstr "Analyseur de cycle"
-msgid ""
-"Cycle Analytics gives an overview of how much time it takes to go from idea "
-"to production in your project."
-msgstr ""
-"L’analyseur de cycle permet d’avoir une vue d’ensemble du temps nécessaire "
-"pour aller d’une idée à sa mise en production pour votre projet."
+msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
+msgstr "L’analyseur de cycle permet d’avoir une vue d’ensemble du temps nécessaire pour aller d’une idée à sa mise en production pour votre projet."
msgid "CycleAnalyticsStage|Code"
msgstr "Code"
@@ -336,12 +435,21 @@ msgid_plural "Deploys"
msgstr[0] "Déploiement"
msgstr[1] "Déploiements"
+msgid "Deploy Keys"
+msgstr ""
+
msgid "Description"
-msgstr "Description"
+msgstr ""
+
+msgid "Details"
+msgstr ""
msgid "Directory name"
msgstr "Nom du dossier"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "Ne plus montrer"
@@ -378,6 +486,27 @@ msgstr "Éditer"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Éditer le pipeline programmé %{id}"
+msgid "Emails"
+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 "Chaque jour (à 4:00 du matin)"
@@ -417,14 +546,25 @@ msgstr[0] "Fourche"
msgstr[1] "Fourches"
msgid "ForkedFromProjectPath|Forked from"
-msgstr "Fouché depuis"
+msgstr "Fourché depuis"
msgid "From issue creation until deploy to production"
msgstr "Depuis la création de l'incident jusqu'au déploiement en production"
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 ""
+
+msgid "Geo Nodes"
+msgstr ""
+
+msgid "Git storage health information has been reset"
+msgstr ""
+
+msgid "GitLab Runner section"
msgstr ""
-"Depuis la fusion de la demande de fusion jusqu'au déploiement en production"
msgid "Go to your fork"
msgstr "Aller à votre fourche"
@@ -432,29 +572,53 @@ msgstr "Aller à votre fourche"
msgid "GoToYourFork|Fork"
msgstr "Fourche"
+msgid "Group overview"
+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 "Home"
msgstr "Accueil"
+msgid "Hooks"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "Maintenance démarrée avec succès"
msgid "Import repository"
msgstr "Importer un dépôt"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Schéma d’intervalle"
msgid "Introducing Cycle Analytics"
msgstr "Introduction à l'analyseur de cycle"
-msgid "Jobs for last month"
-msgstr "Tâches pour le mois dernier"
-
-msgid "Jobs for last week"
-msgstr "Tâches pour la semaine dernière"
+msgid "Issue events"
+msgstr ""
-msgid "Jobs for last year"
-msgstr "Tâches pour l'année dernière"
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "Désactivé"
@@ -462,6 +626,9 @@ msgstr "Désactivé"
msgid "LFSStatus|Enabled"
msgstr "Activé"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "Le dernier %d jour"
@@ -476,6 +643,12 @@ msgstr "Dernière mise à jour"
msgid "Last commit"
msgstr "Dernière validation"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "En apprendre plus dans le"
@@ -488,17 +661,41 @@ msgstr "Quitter le groupe"
msgid "Leave project"
msgstr "Quitter le projet"
+msgid "License"
+msgstr ""
+
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "Limiter l'affichage au plus à %d évènement"
msgstr[1] "Limiter l'affichage au plus à %d évènements"
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "Médian"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "ajouter une clef SSH"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nouvel incident"
@@ -597,6 +794,9 @@ msgstr "Participation"
msgid "NotificationLevel|Watch"
msgstr "Surveillé"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtre"
@@ -604,13 +804,19 @@ msgid "OpenedNDaysAgo|Opened"
msgstr "Ouvert"
msgid "Options"
-msgstr "Options"
+msgstr ""
+
+msgid "Overview"
+msgstr ""
msgid "Owner"
msgstr "Propriétaire"
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
-msgstr "Pipeline"
+msgstr ""
msgid "Pipeline Health"
msgstr "Santé du Pipeline"
@@ -621,6 +827,9 @@ msgstr "Programmation de pipeline"
msgid "Pipeline Schedules"
msgstr "Programmations de pipeline"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Échecs : "
@@ -679,11 +888,20 @@ msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "Personnalisé"
msgid "Pipelines"
-msgstr "Pipelines"
+msgstr ""
msgid "Pipelines charts"
msgstr "Graphique des pipelines"
+msgid "Pipelines for last month"
+msgstr ""
+
+msgid "Pipelines for last week"
+msgstr ""
+
+msgid "Pipelines for last year"
+msgstr ""
+
msgid "Pipeline|all"
msgstr "Tous"
@@ -696,6 +914,15 @@ msgstr "avec l'étape"
msgid "Pipeline|with stages"
msgstr "avec les étapes"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Projet '%{project_name}' en attente de suppression."
@@ -709,8 +936,10 @@ msgid "Project '%{project_name}' will be deleted."
msgstr "Le projet '%{project_name}' sera supprimé."
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 ""
-"L’accès au projet doit être explicitement accordé à chaque utilisateur."
msgid "Project export could not be deleted."
msgstr "L'export du projet n'a pas pu être supprimé."
@@ -718,21 +947,21 @@ msgstr "L'export du projet n'a pas pu être supprimé."
msgid "Project export has been deleted."
msgstr "L'export du projet a été supprimé."
-msgid ""
-"Project export link has expired. Please generate a new export from your "
-"project settings."
-msgstr ""
-"Le lien de l’export du projet a expiré. Merci de générer un nouvel export "
-"depuis les paramètres du projet."
+msgid "Project export link has expired. Please generate a new export from your project settings."
+msgstr "Le lien de l’export du projet a expiré. Merci de générer un nouvel export depuis les paramètres du projet."
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."
+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 ""
+
msgid "ProjectFeature|Disabled"
msgstr "Désactivé"
@@ -754,6 +983,12 @@ msgstr "Étape"
msgid "ProjectNetworkGraph|Graph"
msgstr "Graphique "
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Lire plus"
@@ -790,15 +1025,30 @@ msgstr "Me le rappeler ultérieurement"
msgid "Remove project"
msgstr "Supprimer le projet"
+msgid "Repository"
+msgstr ""
+
msgid "Request Access"
msgstr "Demander l'accès"
+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 "Annuler cette validation"
msgid "Revert this merge request"
msgstr "Annuler cette demande de fusion"
+msgid "SSH Keys"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "Sauvegarder le pipeline programmé"
@@ -817,13 +1067,17 @@ msgstr "Sélectionnez le format de l'archive"
msgid "Select a timezone"
msgstr "Sélectionnez un fuseau horaire"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "Sélectionnez une branche cible"
-msgid "Set a password on your account to pull or push via %{protocol}."
+msgid "Service Templates"
msgstr ""
-"Définissez un mot de passe pour votre compte pour pouvoir tirer ou pousser "
-"par %{protocol}."
+
+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}."
msgid "Set up CI"
msgstr "Mettre en place l'intégration continue (CI)"
@@ -837,20 +1091,35 @@ msgstr "Mettre en place l’auto-déploiement"
msgid "SetPasswordToCloneLink|set a password"
msgstr "définir un mot de passe"
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Affichage de %d évènement"
msgstr[1] "Affichage de %d évènements"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "Code source"
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
msgid "StarProject|Star"
msgstr "S'abonner"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Créer une %{new_merge_request} avec ces changements"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Changer de branche / d'étiquette"
@@ -865,67 +1134,35 @@ msgstr "Étiquettes"
msgid "Target Branch"
msgstr "Branche cible"
-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."
+msgid "Team"
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."
+
+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."
msgid "The collection of events added to the data gathered for that stage."
-msgstr ""
-"L’ensemble d’évènements ajoutés aux données récupérées pour cette étape."
+msgstr "L’ensemble d’évènements ajoutés aux données récupérées pour cette étape."
msgid "The fork relationship has been removed."
msgstr "La relation de fourche a été supprimée."
-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 ""
-"L'étape des incidents montre le temps nécessaire entre la création d'un "
-"incident et son assignation à un jalon, ou son ajout à une liste d'un "
-"tableau d'incidents. Débutez à créer des incidents pour voir des données "
-"pour cette étape."
+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 "L'étape des incidents montre le temps nécessaire entre la création d'un incident et son assignation à un jalon, ou son ajout à une liste d'un tableau d'incidents. Débutez à créer des incidents pour voir des données pour cette étape."
msgid "The phase of the development lifecycle."
msgstr "Les étapes du cycle de développement."
-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 ""
-"Les pipelines programmés exécutent des pipelines dans le futur, de façon "
-"répétée, pour les branches et étiquettes spécifiées. Ces pipelines "
-"programmés héritent d’un accès partiel au projet basé sur l’utilisateur qui "
-"leurs est associé."
+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 "Les pipelines programmés exécutent des pipelines dans le futur, de façon répétée, pour les branches et étiquettes spécifiées. Ces pipelines programmés héritent d’un accès partiel au projet basé sur l’utilisateur qui leurs est associé."
-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 ""
-"L’étape de planification montre le temps entre l’étape précédente et l’envoi "
-"de votre première validation. Ce temps sera automatiquement ajouté quand "
-"vous pousserez votre première validation."
+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 "L’étape de planification montre le temps entre l’étape précédente et l’envoi de votre première validation. Ce temps sera automatiquement ajouté quand vous pousserez votre première validation."
-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 ""
-"L’étape de mise en production montre le temps nécessaire entre la création "
-"d’un incident et le déploiement du code en production. Les données seront "
-"automatiquement ajoutées une fois que vous aurez complété le cycle complet, "
-"depuis l’idée jusqu’à la mise en production."
+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 "L’étape de mise en production montre le temps nécessaire entre la création d’un incident et le déploiement du code en production. Les données seront automatiquement ajoutées une fois que vous aurez complété le cycle complet, depuis l’idée jusqu’à la mise en production."
msgid "The project can be accessed by any logged in user."
-msgstr ""
-"Votre projet peut être accédé par n’importe quel utilisateur authentifié"
+msgstr "Votre projet peut être accédé par n’importe quel utilisateur authentifié"
msgid "The project can be accessed without any authentication."
msgstr "Votre projet peut être accédé sans aucune authentification."
@@ -933,52 +1170,26 @@ msgstr "Votre projet peut être accédé sans aucune authentification."
msgid "The repository for this project does not exist."
msgstr "Le dépôt pour ce projet n'existe pas."
-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 ""
-"L’étape d’évaluation montre le temps entre la création de la demande de "
-"fusion et la fusion effective de celle-ci. Ces données seront "
-"automatiquement ajoutées après que vous ayez fusionné votre première demande "
-"de fusion."
+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 "L’étape d’évaluation montre le temps entre la création de la demande de fusion et la fusion effective de celle-ci. Ces données seront automatiquement ajoutées après que vous ayez fusionné votre première demande de fusion."
-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 ""
-"L’étape de pré-production indique le temps entre la fusion de la DF et le "
-"déploiement du code dans l’environnent de production. Les données seront "
-"automatiquement ajoutées une fois que vous déploierez en production pour la "
-"première fois."
+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 "L’étape de pré-production indique le temps entre la fusion de la DF et le déploiement du code dans l’environnent de production. Les données seront automatiquement ajoutées une fois que vous déploierez en production pour la première fois."
-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 ""
-"L’étape de test montre le temps que le CI de GitLab met pour exécuter chaque "
-"pipeline liés à la demande de fusion. Les données seront automatiquement "
-"ajoutées après que votre premier pipeline s’achèvera."
+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 "L’étape de test montre le temps que le CI de GitLab met pour exécuter chaque pipeline liés à la demande de fusion. Les données seront automatiquement ajoutées après que votre premier pipeline s’achèvera."
msgid "The time taken by each data entry gathered by that stage."
msgstr "Le temps pris par chaque entrée récoltée durant cette étape."
-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 ""
-"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 "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 "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 ""
-"This means you can not push code until you create an empty repository or "
-"import existing one."
+msgid "There are problems accessing Git storage: "
msgstr ""
-"Cela signifie que vous ne pouvez pas pousser du code tant que vous ne créez "
-"pas un dépôt vide, ou importez une dépôt existant."
+
+msgid "This means you can not push code until you create an empty repository or import existing one."
+msgstr "Cela signifie que vous ne pouvez pas pousser du code tant que vous ne créez pas un dépôt vide, ou importez une dépôt existant."
msgid "Time before an issue gets scheduled"
msgstr "Temps avant qu’un incident ne soit planifié"
@@ -1146,6 +1357,9 @@ msgstr "Téléverser un fichier"
msgid "UploadLink|click to upload"
msgstr "Cliquez pour envoyer"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "Utiliser vos paramètres de notification globaux"
@@ -1165,46 +1379,28 @@ msgid "VisibilityLevel|Unknown"
msgstr "Inconnu"
msgid "Want to see the data? Please ask an administrator for access."
-msgstr ""
-"Vous voulez voir les données ? Merci de contacter un administrateur pour en "
-"obtenir l’accès."
+msgstr "Vous voulez voir les données ? Merci de contacter un administrateur pour en obtenir l’accès."
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 ""
+
msgid "Withdraw Access Request"
msgstr "Retirer la demande d'accès"
-msgid ""
-"You are going to remove %{group_name}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Vous êtes sur le point de supprimer %{group_name}. Les groupes supprimés NE "
-"PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr ?"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Vous êtes sur le point de supprimer %{group_name}. Les groupes supprimés NE PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr ?"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Vous êtes sur le point de supprimer %{project_name_with_namespace}.\n"
-"Les projets supprimés NE PEUVENT PAS être restaurés !\n"
-"Êtes vous ABSOLUMENT sûr ? "
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Vous êtes sur le point de supprimer %{project_name_with_namespace}. Les projets supprimés NE PEUVENT PAS être restaurés ! Êtes vous ABSOLUMENT sûr ?"
-msgid ""
-"You are going to remove the fork relationship to source project "
-"%{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr ""
-"Vous allez supprimer la relation de fourche avec le projet source "
-"%{forked_from_project}. Êtes-vous VRAIMENT sûr."
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr "Vous allez supprimer la relation de fourche avec le projet source %{forked_from_project}. Êtes-vous VRAIMENT sûr."
-msgid ""
-"You are going to transfer %{project_name_with_namespace} to another owner. "
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Vous allez transférer %{project_name_with_namespace} à un nouveau "
-"propriétaire. Êtes vous VRAIMENT sûr ?"
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr "Vous allez transférer %{project_name_with_namespace} à un nouveau propriétaire. Êtes vous VRAIMENT sûr ?"
msgid "You can only add files when you are on a branch"
msgstr "Vous ne pouvez ajouter de fichier que dans une branche"
@@ -1222,39 +1418,22 @@ msgid "You will not get any notifications via email"
msgstr "Vous ne recevrez aucune notification par courriel"
msgid "You will only receive notifications for the events you choose"
-msgstr ""
-"Vous ne recevrez de notification que pour les évènements que vous aurez "
-"choisis"
+msgstr "Vous ne recevrez de notification que pour les évènements que vous aurez choisis"
-msgid ""
-"You will only receive notifications for threads you have participated in"
-msgstr ""
-"Vous ne recevrez de notification que pour les sujets auxquels vous avez "
-"participé"
+msgid "You will only receive notifications for threads you have participated in"
+msgstr "Vous ne recevrez de notification que pour les sujets auxquels vous avez participé"
msgid "You will receive notifications for any activity"
msgstr "Vous recevrez des notifications pour n’importe quelles activités"
-msgid ""
-"You will receive notifications only for comments in which you were "
-"@mentioned"
-msgstr ""
-"Vous ne recevrez de notifications que pour les commentaires où vous êtes "
-"@mentionné"
+msgid "You will receive notifications only for comments in which you were @mentioned"
+msgstr "Vous ne recevrez de notifications que pour les commentaires où vous êtes @mentionné"
-msgid ""
-"You won't be able to pull or push project code via %{protocol} until you "
-"%{set_password_link} on your account"
-msgstr ""
-"Vous ne pourrez pas récupérer ou pousser de code par %{protocol} tant que "
-"vous n'aurez pas %{set_password_link} pour votre compte"
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr "Vous ne pourrez pas récupérer ou pousser de code par %{protocol} tant que vous n'aurez pas %{set_password_link} pour votre compte"
-msgid ""
-"You won't be able to pull or push project code via SSH until you "
-"%{add_ssh_key_link} to your profile"
-msgstr ""
-"Vous ne pourrez pas récupérer ou pousser de code par SSH tant que vous "
-"n’aurez pas %{add_ssh_key_link} dans votre profil"
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr "Vous ne pourrez pas récupérer ou pousser de code par SSH tant que vous n’aurez pas %{add_ssh_key_link} dans votre profil"
msgid "Your name"
msgstr "Votre nom"
@@ -1272,6 +1451,6 @@ msgstr "courriels de notification"
msgid "parent"
msgid_plural "parents"
-msgstr[0] "parent"
-msgstr[1] "parents"
+msgstr[0] ""
+msgstr[1] ""
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index babef3ed0af..bd3d5b7c839 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3,13 +3,12 @@
# 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-07-13 12:07-0500\n"
-"PO-Revision-Date: 2017-07-13 12:07-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 08:32+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -31,6 +30,23 @@ msgstr[1] ""
msgid "%{commit_author_link} committed %{commit_timeago}"
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 ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -42,6 +58,18 @@ msgstr ""
msgid "About auto deploy"
msgstr ""
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr ""
@@ -63,15 +91,93 @@ msgstr ""
msgid "Add new directory"
msgstr ""
+msgid "All"
+msgstr ""
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
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 "Attach a file by drag &amp; drop or %{upload_link}"
msgstr ""
+msgid "Authentication log"
+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 "Branch"
msgid_plural "Branches"
msgstr[0] ""
@@ -104,12 +210,18 @@ msgstr ""
msgid "ByAuthor|by"
msgstr ""
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr ""
msgid "Cancel"
msgstr ""
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr ""
@@ -128,6 +240,9 @@ msgstr ""
msgid "Charts"
msgstr ""
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr ""
@@ -188,6 +303,9 @@ msgstr ""
msgid "CiStatus|running"
msgstr ""
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] ""
@@ -220,12 +338,18 @@ msgstr ""
msgid "Compare"
msgstr ""
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr ""
msgid "Contributors"
msgstr ""
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr ""
@@ -235,6 +359,9 @@ 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 ""
@@ -309,12 +436,21 @@ 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 ""
@@ -351,6 +487,27 @@ msgstr ""
msgid "Edit Pipeline Schedule %{id}"
msgstr ""
+msgid "Emails"
+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 ""
@@ -398,34 +555,70 @@ 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 "Group overview"
+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 "Home"
msgstr ""
+msgid "Hooks"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr ""
msgid "Import repository"
msgstr ""
-msgid "Interval Pattern"
+msgid "Install a Runner compatible with GitLab CI"
msgstr ""
-msgid "Introducing Cycle Analytics"
+msgid "Interval Pattern"
msgstr ""
-msgid "Jobs for last month"
+msgid "Introducing Cycle Analytics"
msgstr ""
-msgid "Jobs for last week"
+msgid "Issue events"
msgstr ""
-msgid "Jobs for last year"
+msgid "Issues"
msgstr ""
msgid "LFSStatus|Disabled"
@@ -434,6 +627,9 @@ msgstr ""
msgid "LFSStatus|Enabled"
msgstr ""
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] ""
@@ -448,6 +644,12 @@ msgstr ""
msgid "Last commit"
msgstr ""
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr ""
@@ -460,17 +662,41 @@ 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 "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] ""
@@ -569,6 +795,9 @@ msgstr ""
msgid "NotificationLevel|Watch"
msgstr ""
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr ""
@@ -578,9 +807,15 @@ msgstr ""
msgid "Options"
msgstr ""
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
msgstr ""
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
msgstr ""
@@ -593,6 +828,9 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -656,6 +894,15 @@ 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 ""
@@ -668,6 +915,15 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr ""
@@ -683,6 +939,9 @@ msgstr ""
msgid "Project access must be granted explicitly to each user."
msgstr ""
+msgid "Project details"
+msgstr ""
+
msgid "Project export could not be deleted."
msgstr ""
@@ -698,6 +957,12 @@ msgstr ""
msgid "Project home"
msgstr ""
+msgid "Project overview"
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr ""
@@ -719,6 +984,30 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph"
msgstr ""
+msgid "Push Rules"
+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"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr ""
@@ -755,15 +1044,30 @@ 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 ""
@@ -782,9 +1086,15 @@ 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 ""
@@ -800,20 +1110,35 @@ msgstr ""
msgid "SetPasswordToCloneLink|set a password"
msgstr ""
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] ""
msgstr[1] ""
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr ""
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
msgid "StarProject|Star"
msgstr ""
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr ""
@@ -828,6 +1153,9 @@ 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 ""
@@ -876,6 +1204,9 @@ 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 means you can not push code until you create an empty repository or import existing one."
msgstr ""
@@ -1045,6 +1376,9 @@ msgstr ""
msgid "UploadLink|click to upload"
msgstr ""
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr ""
@@ -1069,19 +1403,16 @@ 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}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
+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}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
+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?"
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index 7ba23d84405..46b3e12f97c 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -1,42 +1,55 @@
-# Huang Tao <htve@outlook.com>, 2017. #zanata
-# Paolo Falomo <info@paolofalomo.it>, 2017. #zanata
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-13 12:07-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Italian\n"
+"Language: it_IT\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Language-Team: Italian (https://translate.zanata.org/project/view/GitLab)\n"
-"PO-Revision-Date: 2017-08-07 10:15-0400\n"
-"Last-Translator: Paolo Falomo <info@paolofalomo.it>\n"
-"Language: it\n"
-"X-Generator: Zanata 3.9.6\n"
-"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: it\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid "%s additional commit has been omitted to prevent performance issues."
-msgid_plural ""
-"%s additional commits have been omitted to prevent performance issues."
+msgid "%d commit"
+msgid_plural "%d commits"
msgstr[0] ""
-"%s commit aggiuntivo è stato omesso per evitare degradi di prestazioni negli "
-"issues."
msgstr[1] ""
-"%s commit aggiuntivi sono stati omessi per evitare degradi di prestazioni "
-"negli issues."
-msgid "%d commit"
-msgid_plural "%d commits"
-msgstr[0] "%d commit"
-msgstr[1] "%d commit"
+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 commit aggiuntivo è stato omesso per evitare degradi di prestazioni negli issues."
+msgstr[1] "%s commit aggiuntivi sono stati omessi per evitare degradi di prestazioni negli issues."
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} ha committato %{commit_timeago}"
+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 ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "1 pipeline"
-msgstr[1] "%d pipeline"
+msgstr[0] ""
+msgstr[1] ""
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Un insieme di grafici riguardo la Continuous Integration"
@@ -44,6 +57,18 @@ msgstr "Un insieme di grafici riguardo la Continuous Integration"
msgid "About auto deploy"
msgstr "Riguardo il rilascio automatico"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr "Attivo"
@@ -60,36 +85,105 @@ msgid "Add License"
msgstr "Aggiungi Licenza"
msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr ""
-"Aggiungi una chiave SSH al tuo profilo per eseguire pull o push tramite SSH"
+msgstr "Aggiungi una chiave SSH al tuo profilo per eseguire pull o push tramite SSH"
msgid "Add new directory"
msgstr "Aggiungi una directory (cartella)"
+msgid "All"
+msgstr ""
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "Progetto archiviato! La Repository è sola-lettura"
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Sei sicuro di voler cancellare questa pipeline programmata?"
+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 "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"
+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 ""
-"Aggiungi un file tramite trascina &amp; rilascia ( drag &amp; drop) o "
-"%{upload_link}"
msgid "Branch"
msgid_plural "Branches"
-msgstr[0] "Branch"
-msgstr[1] "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 ""
-"La branch <strong>%{branch_name}</strong> è stata creata. Per impostare un "
-"rilascio automatico scegli un template CI di Gitlab e committa le tue "
-"modifiche %{link_to_autodeploy_doc}"
+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 branch <strong>%{branch_name}</strong> è stata creata. Per impostare un rilascio automatico scegli un template CI di Gitlab e committa le tue modifiche %{link_to_autodeploy_doc}"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Cerca branches"
@@ -98,7 +192,7 @@ msgid "BranchSwitcherTitle|Switch branch"
msgstr "Cambia branch"
msgid "Branches"
-msgstr "Branches"
+msgstr ""
msgid "Browse Directory"
msgstr "Naviga direttori"
@@ -115,12 +209,18 @@ msgstr "Guarda i files"
msgid "ByAuthor|by"
msgstr "per"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "Configurazione CI (Integrazione Continua)"
msgid "Cancel"
msgstr "Cancella"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Preleva nella branch"
@@ -134,13 +234,16 @@ msgid "ChangeTypeAction|Revert"
msgstr "Ripristina"
msgid "Changelog"
-msgstr "Changelog"
+msgstr ""
msgid "Charts"
msgstr "Grafici"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
-msgstr "Cherry-pick this commit"
+msgstr ""
msgid "Cherry-pick this merge request"
msgstr "Cherry-pick questa richiesta di merge"
@@ -199,10 +302,13 @@ msgstr "saltata"
msgid "CiStatus|running"
msgstr "in corso"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
-msgstr[0] "Commit"
-msgstr[1] "Commits"
+msgstr[0] ""
+msgstr[1] ""
msgid "Commit duration in minutes for last 30 commits"
msgstr "Durata del commit (in minuti) per gli ultimi 30 commit"
@@ -217,7 +323,7 @@ msgid "CommitMessage|Add %{file_name}"
msgstr "Aggiungi %{file_name}"
msgid "Commits"
-msgstr "Commits"
+msgstr ""
msgid "Commits feed"
msgstr "Feed dei Commits"
@@ -231,12 +337,18 @@ msgstr "Committato da "
msgid "Compare"
msgstr "Confronta"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "Guida per contribuire"
msgid "Contributors"
msgstr "Collaboratori"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "Copia URL negli appunti"
@@ -246,12 +358,11 @@ msgstr "Copia l'SHA del commit negli appunti"
msgid "Create New Directory"
msgstr "Crea una nuova cartella"
-msgid ""
-"Create a personal access token on your account to pull or push via "
-"%{protocol}."
+msgid "Create a new branch"
msgstr ""
-"Creare un token di accesso sul tuo account per eseguire pull o push tramite "
-"%{protocol}"
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr "Creare un token di accesso sul tuo account per eseguire pull o push tramite %{protocol}"
msgid "Create directory"
msgstr "Crea cartella"
@@ -275,7 +386,7 @@ msgid "CreateTokenToCloneLink|create a personal access token"
msgstr "Crea token d'accesso personale"
msgid "Cron Timezone"
-msgstr "Cron Timezone"
+msgstr ""
msgid "Cron syntax"
msgstr "Sintassi Cron"
@@ -283,24 +394,14 @@ msgstr "Sintassi Cron"
msgid "Custom notification events"
msgstr "Eventi-Notifica personalizzati"
-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 ""
-"I livelli di notifica personalizzati sono uguali a quelli di partecipazione. "
-"Con i livelli di notifica personalizzati riceverai anche notifiche per gli "
-"eventi da te scelti %{notification_link}."
+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 "I livelli di notifica personalizzati sono uguali a quelli di partecipazione. Con i livelli di notifica personalizzati riceverai anche notifiche per gli eventi da te scelti %{notification_link}."
msgid "Cycle Analytics"
msgstr "Statistiche Cicliche"
-msgid ""
-"Cycle Analytics gives an overview of how much time it takes to go from idea "
-"to production in your project."
-msgstr ""
-"L'Analisi Ciclica fornisce una panoramica sul tempo che trascorre tra l'idea "
-"ed il rilascio in produzione del tuo progetto"
+msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
+msgstr "L'Analisi Ciclica fornisce una panoramica sul tempo che trascorre tra l'idea ed il rilascio in produzione del tuo progetto"
msgid "CycleAnalyticsStage|Code"
msgstr "Codice"
@@ -334,12 +435,21 @@ msgid_plural "Deploys"
msgstr[0] "Rilascio"
msgstr[1] "Rilasci"
+msgid "Deploy Keys"
+msgstr ""
+
msgid "Description"
msgstr "Descrizione"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "Nome cartella"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "Non mostrare più"
@@ -376,6 +486,27 @@ msgstr "Modifica"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Cambia programmazione della pipeline %{id}"
+msgid "Emails"
+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 "Ogni giorno (alle 4 del mattino)"
@@ -392,7 +523,7 @@ msgid "Failed to remove the pipeline schedule"
msgstr "Impossibile rimuovere la pipeline pianificata"
msgid "Files"
-msgstr "Files"
+msgstr ""
msgid "Filter by commit message"
msgstr "Filtra per messaggio di commit"
@@ -411,8 +542,8 @@ msgstr "Push di"
msgid "Fork"
msgid_plural "Forks"
-msgstr[0] "Fork"
-msgstr[1] "Forks"
+msgstr[0] ""
+msgstr[1] ""
msgid "ForkedFromProjectPath|Forked from"
msgstr "Fork da"
@@ -421,9 +552,19 @@ msgid "From issue creation until deploy to production"
msgstr "Dalla creazione di un issue fino al rilascio in produzione"
msgid "From merge request merge until deploy to production"
+msgstr "Dalla richiesta di merge fino effettua il merge fino al rilascio in produzione"
+
+msgid "GPG Keys"
+msgstr ""
+
+msgid "Geo Nodes"
+msgstr ""
+
+msgid "Git storage health information has been reset"
+msgstr ""
+
+msgid "GitLab Runner section"
msgstr ""
-"Dalla richiesta di merge fino effettua il merge fino al rilascio in "
-"produzione"
msgid "Go to your fork"
msgstr "Vai il tuo fork"
@@ -431,8 +572,32 @@ msgstr "Vai il tuo fork"
msgid "GoToYourFork|Fork"
msgstr "Fork"
+msgid "Group overview"
+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 "Home"
-msgstr "Home"
+msgstr ""
+
+msgid "Hooks"
+msgstr ""
msgid "Housekeeping successfully started"
msgstr "Housekeeping iniziato con successo"
@@ -440,20 +605,20 @@ msgstr "Housekeeping iniziato con successo"
msgid "Import repository"
msgstr "Importa repository"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Intervallo di Pattern"
msgid "Introducing Cycle Analytics"
msgstr "Introduzione delle Analisi Cicliche"
-msgid "Jobs for last month"
-msgstr "Jobs dell'ultimo mese"
-
-msgid "Jobs for last week"
-msgstr "Jobs dell'ultima settimana"
+msgid "Issue events"
+msgstr ""
-msgid "Jobs for last year"
-msgstr "Jobs dell'ultimo anno"
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "Disabilitato"
@@ -461,6 +626,9 @@ msgstr "Disabilitato"
msgid "LFSStatus|Enabled"
msgstr "Abilitato"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "L'ultimo %d giorno"
@@ -475,6 +643,12 @@ msgstr "Ultimo Aggiornamento"
msgid "Last commit"
msgstr "Ultimo Commit"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "Leggi di più su"
@@ -487,17 +661,41 @@ msgstr "Abbandona il gruppo"
msgid "Leave project"
msgstr "Abbandona il progetto"
+msgid "License"
+msgstr ""
+
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "Limita visualizzazione %d d'evento"
msgstr[1] "Limita visualizzazione %d di eventi"
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "Mediano"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "aggiungi una chiave SSH"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nuovo Issue"
@@ -596,6 +794,9 @@ msgstr "Partecipa"
msgid "NotificationLevel|Watch"
msgstr "Osserva"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtra"
@@ -605,11 +806,17 @@ msgstr "Aperto"
msgid "Options"
msgstr "Opzioni"
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
-msgstr "Owner"
+msgstr ""
+
+msgid "Password"
+msgstr ""
msgid "Pipeline"
-msgstr "Pipeline"
+msgstr ""
msgid "Pipeline Health"
msgstr "Stato della Pipeline"
@@ -620,6 +827,9 @@ msgstr "Pianificazione Pipeline"
msgid "Pipeline Schedules"
msgstr "Pianificazione multipla Pipeline"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Fallita:"
@@ -683,6 +893,15 @@ msgstr "Pipeline"
msgid "Pipelines charts"
msgstr "Grafici pipeline"
+msgid "Pipelines for last month"
+msgstr ""
+
+msgid "Pipelines for last week"
+msgstr ""
+
+msgid "Pipelines for last year"
+msgstr ""
+
msgid "Pipeline|all"
msgstr "tutto"
@@ -695,6 +914,15 @@ msgstr "con stadio"
msgid "Pipeline|with stages"
msgstr "con più stadi"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Il Progetto '%{project_name}' in coda di eliminazione."
@@ -710,27 +938,30 @@ msgstr "Il Progetto '%{project_name}' verrà eliminato"
msgid "Project access must be granted explicitly to each user."
msgstr "L'accesso al progetto dev'esser fornito esplicitamente ad ogni utente"
+msgid "Project details"
+msgstr ""
+
msgid "Project export could not be deleted."
msgstr "L'esportazione del progetto non può essere eliminata."
msgid "Project export has been deleted."
msgstr "L'esportazione del progetto è stata eliminata."
-msgid ""
-"Project export link has expired. Please generate a new export from your "
-"project settings."
-msgstr ""
-"Il link d'esportazione del progetto è scaduto. Genera una nuova esportazione "
-"dalle impostazioni del tuo progetto."
+msgid "Project export link has expired. Please generate a new export from your project settings."
+msgstr "Il link d'esportazione del progetto è scaduto. Genera una nuova esportazione dalle impostazioni del tuo progetto."
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."
+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 ""
+
msgid "ProjectFeature|Disabled"
msgstr "Disabilitato"
@@ -752,6 +983,12 @@ msgstr "Stadio"
msgid "ProjectNetworkGraph|Graph"
msgstr "Grafico"
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Vedi altro"
@@ -788,15 +1025,30 @@ msgstr "Ricordamelo più tardi"
msgid "Remove project"
msgstr "Rimuovi progetto"
+msgid "Repository"
+msgstr ""
+
msgid "Request Access"
msgstr "Richiedi accesso"
+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 "Ripristina questo commit"
msgid "Revert this merge request"
msgstr "Ripristina questa richiesta di merge"
+msgid "SSH Keys"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "Salva pianificazione pipeline"
@@ -815,13 +1067,17 @@ msgstr "Seleziona formato d'archivio"
msgid "Select a timezone"
msgstr "Seleziona una timezone"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "Seleziona una branch di destinazione"
-msgid "Set a password on your account to pull or push via %{protocol}."
+msgid "Service Templates"
msgstr ""
-"Establezca una contraseña en su cuenta para actualizar o enviar a través de "
-"%{protocol}."
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr "Establezca una contraseña en su cuenta para actualizar o enviar a través de %{protocol}."
msgid "Set up CI"
msgstr "Configura CI"
@@ -835,42 +1091,54 @@ msgstr "Configura il rilascio automatico"
msgid "SetPasswordToCloneLink|set a password"
msgstr "imposta una password"
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Visualizza %d evento"
msgstr[1] "Visualizza %d eventi"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "Codice Sorgente"
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
msgid "StarProject|Star"
msgstr "Star"
msgid "Start a %{new_merge_request} with these changes"
msgstr "inizia una %{new_merge_request} con queste modifiche"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Cambia branch/tag"
msgid "Tag"
msgid_plural "Tags"
-msgstr[0] "Tag"
-msgstr[1] "Tags"
+msgstr[0] ""
+msgstr[1] ""
msgid "Tags"
-msgstr "Tags"
+msgstr ""
msgid "Target Branch"
msgstr "Branch di destinazione"
-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."
+msgid "Team"
msgstr ""
-"Lo stadio di programmazione mostra il tempo trascorso dal primo commit alla "
-"creazione di una richiesta di merge (MR). I dati saranno aggiunti una volta "
-"che avrai creato la prima richiesta di merge."
+
+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 "Lo stadio di programmazione mostra il tempo trascorso dal primo commit alla creazione di una richiesta di merge (MR). I dati saranno aggiunti una volta che avrai creato la prima richiesta di merge."
msgid "The collection of events added to the data gathered for that stage."
msgstr "L'insieme di eventi aggiunti ai dati raccolti per quello stadio."
@@ -878,102 +1146,50 @@ msgstr "L'insieme di eventi aggiunti ai dati raccolti per quello stadio."
msgid "The fork relationship has been removed."
msgstr "La relazione del fork è stata rimossa"
-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 ""
-"Lo stadio di Issue mostra il tempo che impiega un issue ad esser correlato "
-"ad una Milestone, o ad esser aggiunto ad una tua Lavagna. Inizia la "
-"creazione di problemi per visualizzare i dati in questo stadio."
+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 "Lo stadio di Issue mostra il tempo che impiega un issue ad esser correlato ad una Milestone, o ad esser aggiunto ad una tua Lavagna. Inizia la creazione di problemi per visualizzare i dati in questo stadio."
msgid "The phase of the development lifecycle."
msgstr "Il ciclo vitale della fase di sviluppo."
-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 ""
-"Le pipelines pianificate vengono eseguite nel futuro, ripetitivamente, per "
-"specifici tag o branch ed ereditano restrizioni di progetto basate "
-"sull'utente ad esse associato."
+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 "Le pipelines pianificate vengono eseguite nel futuro, ripetitivamente, per specifici tag o branch ed ereditano restrizioni di progetto basate sull'utente ad esse associato."
-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 ""
-"Lo stadio di pianificazione mostra il tempo trascorso dal primo commit al "
-"suo step precedente. Questo periodo sarà disponibile automaticamente nel "
-"momento in cui farai il primo commit."
+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 "Lo stadio di pianificazione mostra il tempo trascorso dal primo commit al suo step precedente. Questo periodo sarà disponibile automaticamente nel momento in cui farai il primo commit."
-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 ""
-"Lo stadio di produzione mostra il tempo totale che trascorre tra la "
-"creazione di un issue il suo rilascio (inteso come codice) in produzione. "
-"Questo dato sarà disponibile automaticamente nel momento in cui avrai "
-"completato l'intero processo ideale del ciclo di produzione"
+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 "Lo stadio di produzione mostra il tempo totale che trascorre tra la creazione di un issue il suo rilascio (inteso come codice) in produzione. Questo dato sarà disponibile automaticamente nel momento in cui avrai completato l'intero processo ideale del ciclo di produzione"
msgid "The project can be accessed by any logged in user."
msgstr "Qualunque utente autenticato può accedere a questo progetto."
msgid "The project can be accessed without any authentication."
-msgstr ""
-"Chiunque può accedere a questo progetto (senza alcuna autenticazione)."
+msgstr "Chiunque può accedere a questo progetto (senza alcuna autenticazione)."
msgid "The repository for this project does not exist."
msgstr "La repository di questo progetto non esiste."
-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 ""
-"Lo stadio di revisione mostra il tempo tra una richiesta di merge al suo "
-"svolgimento effettivo. Questo dato sarà disponibile appena avrai completato "
-"una MR (Merger Request)"
+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 "Lo stadio di revisione mostra il tempo tra una richiesta di merge al suo svolgimento effettivo. Questo dato sarà disponibile appena avrai completato una MR (Merger Request)"
-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 ""
-"Lo stadio di pre-rilascio mostra il tempo che trascorre da una MR (Richiesta "
-"di Merge) completata al suo rilascio in ambiente di produzione. Questa "
-"informazione sarà disponibile dal tuo primo rilascio in produzione"
+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 "Lo stadio di pre-rilascio mostra il tempo che trascorre da una MR (Richiesta di Merge) completata al suo rilascio in ambiente di produzione. Questa informazione sarà disponibile dal tuo primo rilascio in produzione"
-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 ""
-"Lo stadio di test mostra il tempo che ogni Pipeline impiega per essere "
-"eseguita in ogni Richiesta di Merge correlata. L'informazione sarà "
-"disponibile automaticamente quando la tua prima Pipeline avrà finito d'esser "
-"eseguita."
+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 "Lo stadio di test mostra il tempo che ogni Pipeline impiega per essere eseguita in ogni Richiesta di Merge correlata. L'informazione sarà disponibile automaticamente quando la tua prima Pipeline avrà finito d'esser eseguita."
msgid "The time taken by each data entry gathered by that stage."
-msgstr ""
-"Il tempo aggregato relativo eventi/data entry raccolto in quello stadio."
+msgstr "Il tempo aggregato relativo eventi/data entry raccolto in quello stadio."
-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 ""
-"Il valore falsato nel mezzo di una serie di dati osservati. ES: tra 3,5,9 il "
-"mediano è 5. Tra 3,5,7,8 il mediano è (5+7)/2 quindi 6."
+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 "Il valore falsato nel mezzo di una serie di dati osservati. ES: tra 3,5,9 il mediano è 5. Tra 3,5,7,8 il mediano è (5+7)/2 quindi 6."
-msgid ""
-"This means you can not push code until you create an empty repository or "
-"import existing one."
+msgid "There are problems accessing Git storage: "
msgstr ""
-"Questo significa che non è possibile effettuare push di codice fino a che "
-"non crei una repository vuota o ne importi una esistente"
+
+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"
msgid "Time before an issue gets scheduled"
msgstr "Il tempo che impiega un issue per esser pianificato"
@@ -1130,7 +1346,7 @@ msgid "Total test time for all commits/merges"
msgstr "Tempo totale di test per tutti i commits/merges"
msgid "Unstar"
-msgstr "Unstar"
+msgstr ""
msgid "Upload New File"
msgstr "Carica un nuovo file"
@@ -1141,6 +1357,9 @@ msgstr "Carica file"
msgid "UploadLink|click to upload"
msgstr "clicca per caricare"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "Usa le tue impostazioni globali "
@@ -1160,46 +1379,28 @@ msgid "VisibilityLevel|Unknown"
msgstr "Sconosciuto"
msgid "Want to see the data? Please ask an administrator for access."
-msgstr ""
-"Vuoi visualizzare i dati? Richiedi l'accesso ad un amministratore, grazie."
+msgstr "Vuoi visualizzare i dati? Richiedi l'accesso ad un amministratore, grazie."
msgid "We don't have enough data to show this stage."
msgstr "Non ci sono sufficienti dati da mostrare su questo stadio"
+msgid "Wiki"
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr "Ritira richiesta d'accesso"
-msgid ""
-"You are going to remove %{group_name}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Stai per rimuovere il gruppo %{group_name}.\n"
-"I gruppi rimossi NON possono esser ripristinati!\n"
-"Sei ASSOLUTAMENTE sicuro?"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Stai per rimuovere il gruppo %{group_name}. I gruppi rimossi NON POSSONO esser ripristinati! Sei ASSOLUTAMENTE sicuro?"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Stai per rimuovere %{project_name_with_namespace}.\n"
-"I progetti rimossi NON POSSONO essere ripristinati\n"
-"Sei assolutamente sicuro?"
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Stai per rimuovere %{project_name_with_namespace}. I progetti rimossi NON POSSONO essere ripristinati! Sei assolutamente sicuro?"
-msgid ""
-"You are going to remove the fork relationship to source project "
-"%{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr ""
-"Stai per rimuovere la relazione con il progetto sorgente "
-"%{forked_from_project}. Sei ASSOLUTAMENTE sicuro?"
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr "Stai per rimuovere la relazione con il progetto sorgente %{forked_from_project}. Sei ASSOLUTAMENTE sicuro?"
-msgid ""
-"You are going to transfer %{project_name_with_namespace} to another owner. "
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Stai per trasferire %{project_name_with_namespace} ad un altro owner. Sei "
-"ASSOLUTAMENTE sicuro?"
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr "Stai per trasferire %{project_name_with_namespace} ad un altro owner. Sei ASSOLUTAMENTE sicuro?"
msgid "You can only add files when you are on a branch"
msgstr "Puoi aggiungere files solo quando sei in una branch"
@@ -1219,31 +1420,20 @@ msgstr "Non riceverai alcuna notifica via email"
msgid "You will only receive notifications for the events you choose"
msgstr "Riceverai notifiche solo per gli eventi che hai scelto"
-msgid ""
-"You will only receive notifications for threads you have participated in"
+msgid "You will only receive notifications for threads you have participated in"
msgstr "Riceverai notifiche solo per i threads a cui hai partecipato"
msgid "You will receive notifications for any activity"
msgstr "Riceverai notifiche per ogni attività"
-msgid ""
-"You will receive notifications only for comments in which you were "
-"@mentioned"
+msgid "You will receive notifications only for comments in which you were @mentioned"
msgstr "Riceverai notifiche solo per i commenti ai quale sei stato menzionato"
-msgid ""
-"You won't be able to pull or push project code via %{protocol} until you "
-"%{set_password_link} on your account"
-msgstr ""
-"Non sarai in grado di eseguire pull o push di codice tramite %{protocol} "
-"fino a che %{set_password_link} nel tuo account."
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr "Non sarai in grado di eseguire pull o push di codice tramite %{protocol} fino a che %{set_password_link} nel tuo account."
-msgid ""
-"You won't be able to pull or push project code via SSH until you "
-"%{add_ssh_key_link} to your profile"
-msgstr ""
-"Non sarai in grado di effettuare push o pull tramite SSH fino a che "
-"%{add_ssh_key_link} al tuo profilo"
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr "Non sarai in grado di effettuare push o pull tramite SSH fino a che %{add_ssh_key_link} al tuo profilo"
msgid "Your name"
msgstr "Il tuo nome"
@@ -1261,6 +1451,6 @@ msgstr "Notifiche via email"
msgid "parent"
msgid_plural "parents"
-msgstr[0] "parent"
-msgstr[1] "parents"
+msgstr[0] ""
+msgstr[1] ""
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index 0b1db651c11..bc25b69c80a 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -1,40 +1,51 @@
-# Arthur Charron <arthur.charron@hotmail.fr>, 2017. #zanata
-# Huang Tao <htve@outlook.com>, 2017. #zanata
-# Kohei Ota <inductor@kela.jp>, 2017. #zanata
-# Taisuke Inoue <taisuke.inoue.jp@gmail.com>, 2017. #zanata
-# Takuya Noguchi <takninnovationresearch@gmail.com>, 2017. #zanata
-# YANO Tethurou <tetuyano+zana@gmail.com>, 2017. #zanata
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-13 12:07-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Japanese\n"
+"Language: ja_JP\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-08-06 11:23-0400\n"
-"Last-Translator: Taisuke Inoue <taisuke.inoue.jp@gmail.com>\n"
-"Language-Team: Japanese "Language-Team: Russian (https://translate.zanata.org/project/view/GitLab)\n"
-"Language: ja\n"
-"X-Generator: Zanata 3.9.6\n"
-"Plural-Forms: nplurals=1; plural=0\n"
-
-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 個ã®ã‚³ãƒŸãƒƒãƒˆã‚’çœç•¥ã—ã¾ã—ãŸã€‚"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: ja\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d個ã®ã‚³ãƒŸãƒƒãƒˆ"
+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 個ã®ã‚³ãƒŸãƒƒãƒˆã‚’çœç•¥ã—ã¾ã—ãŸã€‚"
+
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_timeago}ã«%{commit_author_link}ãŒã‚³ãƒŸãƒƒãƒˆã—ã¾ã—ãŸã€‚"
+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] ""
+
+msgid "(checkout the %{link} for information on how to install it)."
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "1 個ã®ãƒ‘イプライン"
-msgstr[1] "%d 個ã®ãƒ‘イプライン"
+msgstr[0] "%d 個ã®ãƒ‘イプライン"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "CIã«ã¤ã„ã¦ã®ã‚°ãƒ©ãƒ•"
@@ -42,6 +53,18 @@ msgstr "CIã«ã¤ã„ã¦ã®ã‚°ãƒ©ãƒ•"
msgid "About auto deploy"
msgstr "自動デプロイã«ã¤ã„ã¦"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr "有効"
@@ -63,26 +86,99 @@ msgstr "SSHã§ãƒ—ルやプッシュã™ã‚‹å ´åˆã¯ã€ãƒ—ロフィールã«SSHéµ
msgid "Add new directory"
msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’追加"
+msgid "All"
+msgstr ""
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
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 "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "ドラッグ&ドロップã¾ãŸã¯ %{upload_link} ã§ãƒ•ã‚¡ã‚¤ãƒ«ã‚’添付"
+msgid "Authentication log"
+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 "Branch"
msgid_plural "Branches"
msgstr[0] "ブランãƒ"
-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 ""
-"<strong>%{branch_name}</strong> ブランãƒãŒä½œæˆã•ã‚Œã¾ã—ãŸã€‚自動デプロイを設定ã™ã‚‹ã«ã¯ã€GitLab CI Yaml "
-"テンプレートをé¸æŠžã—ã¦ã€å¤‰æ›´ã‚’コミットã—ã¦ãã ã•ã„。 %{link_to_autodeploy_doc}"
+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 "<strong>%{branch_name}</strong> ブランãƒãŒä½œæˆã•ã‚Œã¾ã—ãŸã€‚自動デプロイを設定ã™ã‚‹ã«ã¯ã€GitLab CI Yaml テンプレートをé¸æŠžã—ã¦ã€å¤‰æ›´ã‚’コミットã—ã¦ãã ã•ã„。 %{link_to_autodeploy_doc}"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ブランãƒã‚’検索"
@@ -108,12 +204,18 @@ msgstr "ファイルを表示"
msgid "ByAuthor|by"
msgstr "作者"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "CI 設定"
msgid "Cancel"
msgstr "キャンセル"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "ピック先ブランãƒ:"
@@ -132,6 +234,9 @@ msgstr "変更履歴"
msgid "Charts"
msgstr "ãƒãƒ£ãƒ¼ãƒˆ"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "ã“ã®ã‚³ãƒŸãƒƒãƒˆã‚’ãƒã‚§ãƒªãƒ¼ãƒ”ック"
@@ -192,6 +297,9 @@ msgstr "スキップ済ã¿"
msgid "CiStatus|running"
msgstr "実行中"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "コミット"
@@ -223,12 +331,18 @@ msgstr "コミット担当者: "
msgid "Compare"
msgstr "比較"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "貢献者å‘ã‘ガイド"
msgid "Contributors"
msgstr "貢献者"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "クリップボードã«URLをコピー"
@@ -238,9 +352,10 @@ msgstr "コミットã®SHAをクリップボードã«ã‚³ãƒ”ー"
msgid "Create New Directory"
msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’作æˆ"
-msgid ""
-"Create a personal access token on your account to pull or push via "
-"%{protocol}."
+msgid "Create a new branch"
+msgstr ""
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "%{protocol} ã§ãƒ—ッシュやプルã™ã‚‹ãŸã‚ã®ã‚ãªãŸå€‹äººç”¨ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’作æˆ"
msgid "Create directory"
@@ -273,23 +388,14 @@ msgstr "Cron ã®æ§‹æ–‡"
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 ""
-"\"カスタム\" ã®é€šçŸ¥ãƒ¬ãƒ™ãƒ«ã®åŸºæœ¬ã¯ \"å‚加\" "
-"ã¨åŒã˜ã§ã™ã€‚ã¾ãŸã€ã‚«ã‚¹ã‚¿ãƒ é€šçŸ¥ã«è¨­å®šã™ã‚‹ã“ã¨ã§é¸æŠžã—ãŸã‚«ã‚¹ã‚¿ãƒ ã‚¤ãƒ™ãƒ³ãƒˆã®é€šçŸ¥ã‚’å—ã‘å–ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚ã‚‚ã£ã¨è©³ã—ã知りãŸã„å ´åˆã¯ "
-"%{notification_link} を見ã¦ãã ã•ã„。"
+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 "\"カスタム\" ã®é€šçŸ¥ãƒ¬ãƒ™ãƒ«ã®åŸºæœ¬ã¯ \"å‚加\" ã¨åŒã˜ã§ã™ã€‚ã¾ãŸã€ã‚«ã‚¹ã‚¿ãƒ é€šçŸ¥ã«è¨­å®šã™ã‚‹ã“ã¨ã§é¸æŠžã—ãŸã‚«ã‚¹ã‚¿ãƒ ã‚¤ãƒ™ãƒ³ãƒˆã®é€šçŸ¥ã‚’å—ã‘å–ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚ã‚‚ã£ã¨è©³ã—ã知りãŸã„å ´åˆã¯ %{notification_link} を見ã¦ãã ã•ã„。"
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 "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 "コード"
@@ -322,12 +428,21 @@ msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "デプロイ"
+msgid "Deploy Keys"
+msgstr ""
+
msgid "Description"
msgstr "説明"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "ディレクトリå"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "次回ã‹ã‚‰è¡¨ç¤ºã—ãªã„"
@@ -364,6 +479,27 @@ msgstr "編集"
msgid "Edit Pipeline Schedule %{id}"
msgstr "パイプラインスケジュール %{id} を編集"
+msgid "Emails"
+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 "毎日 (åˆå‰4:00)"
@@ -410,35 +546,71 @@ 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 "Group overview"
+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 "Home"
msgstr "ホーム"
+msgid "Hooks"
+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 "Jobs for last month"
-msgstr "先月ã®ã‚¸ãƒ§ãƒ–"
-
-msgid "Jobs for last week"
-msgstr "先週ã®ã‚¸ãƒ§ãƒ–"
+msgid "Issue events"
+msgstr ""
-msgid "Jobs for last year"
-msgstr "昨年ã®ã‚¸ãƒ§ãƒ–"
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "無効"
@@ -446,6 +618,9 @@ msgstr "無効"
msgid "LFSStatus|Enabled"
msgstr "有効"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "éŽåŽ»%d日間"
@@ -459,6 +634,12 @@ msgstr "最新アップデート"
msgid "Last commit"
msgstr "最新コミット"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "詳ã—ã見る:"
@@ -471,16 +652,40 @@ 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] "イベント表示数を最大 %d 個ã«åˆ¶é™"
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "中央値"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "SSH éµã‚’追加"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "æ–°è¦èª²é¡Œ"
@@ -578,6 +783,9 @@ msgstr "å‚加"
msgid "NotificationLevel|Watch"
msgstr "ã™ã¹ã¦é€šçŸ¥"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "フィルター"
@@ -587,9 +795,15 @@ msgstr "オープンã•ã‚ŒãŸã®ã¯"
msgid "Options"
msgstr "オプション"
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
msgstr "オーナー"
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
msgstr "パイプライン"
@@ -602,6 +816,9 @@ msgstr "パイプラインスケジュール"
msgid "Pipeline Schedules"
msgstr "パイプラインスケジュール"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "失敗:"
@@ -665,6 +882,15 @@ 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 "全件"
@@ -677,6 +903,15 @@ msgstr "ステージã‚ã‚Š"
msgid "Pipeline|with stages"
msgstr "ステージã‚ã‚Š"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "'%{project_name}' プロジェクトã¯å‰Šé™¤å‡¦ç†å¾…ã¡ã§ã™ã€‚"
@@ -692,15 +927,16 @@ msgstr "'%{project_name}' プロジェクトã¯å‰Šé™¤ã•ã‚Œã¾ã™ã€‚"
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."
+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."
@@ -709,6 +945,12 @@ msgstr "プロジェクトã®ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã‚’開始ã—ã¾ã—ãŸã€‚ダウン
msgid "Project home"
msgstr "プロジェクトホーム"
+msgid "Project overview"
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "無効"
@@ -730,11 +972,17 @@ msgstr "ステージ"
msgid "ProjectNetworkGraph|Graph"
msgstr "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‚°ãƒ©ãƒ•"
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "続ãを読む"
msgid "Readme"
-msgstr "Readme"
+msgstr ""
msgid "RefSwitcher|Branches"
msgstr "ブランãƒ"
@@ -766,15 +1014,30 @@ 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 "パイプラインスケジュールをä¿å­˜"
@@ -793,9 +1056,15 @@ 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 "%{protocol} プロコトル経由ã§ãƒ—ルã€ãƒ—ッシュã™ã‚‹ãŸã‚ã«ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ãƒ‘スワードを設定。"
@@ -811,19 +1080,34 @@ msgstr "自動デプロイを設定"
msgid "SetPasswordToCloneLink|set a password"
msgstr "パスワードを設定"
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "%d ã®ã‚¤ãƒ™ãƒ³ãƒˆã‚’表示中"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "ソースコード"
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
msgid "StarProject|Star"
msgstr "スターを付ã‘ã‚‹"
msgid "Start a %{new_merge_request} with these changes"
msgstr "ã“ã®å¤‰æ›´ã§ %{new_merge_request} を作æˆã™ã‚‹"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "ブランãƒãƒ»ã‚¿ã‚°åˆ‡ã‚Šæ›¿ãˆ"
@@ -837,12 +1121,11 @@ msgstr "ã‚¿ã‚°"
msgid "Target Branch"
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."
+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 "ã“ã®ã‚¹ãƒ†ãƒ¼ã‚¸ã§è¨ˆæ¸¬ãƒ‡ãƒ¼ã‚¿ã«è¿½åŠ ã•ã‚ŒãŸã‚¤ãƒ™ãƒ³ãƒˆãƒªã‚¹ãƒˆ"
@@ -850,36 +1133,20 @@ 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 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 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 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 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 "プロジェクトã¯ã€ãƒ­ã‚°ã‚¤ãƒ³ãƒ¦ãƒ¼ã‚¶ãƒ¼ã§ã‚ã‚Œã°èª°ã§ã‚‚アクセスã§ãã¾ã™ã€‚"
@@ -890,42 +1157,25 @@ 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 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 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 ""
-"テスティングステージã§ã¯ã€GitLab CI "
-"ãŒé–¢é€£ã™ã‚‹ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®å„パイプラインを実行ã™ã‚‹æ™‚é–“ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®ãƒ‡ãƒ¼ã‚¿ã¯æœ€åˆã®ãƒ‘イプラインãŒå®Œäº†ã—ãŸã¨ãã«è‡ªå‹•çš„ã«è¿½åŠ ã•ã‚Œã¾ã™ã€‚"
+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 "テスティングステージã§ã¯ã€GitLab CI ãŒé–¢é€£ã™ã‚‹ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®å„パイプラインを実行ã™ã‚‹æ™‚é–“ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®ãƒ‡ãƒ¼ã‚¿ã¯æœ€åˆã®ãƒ‘イプラインãŒå®Œäº†ã—ãŸã¨ãã«è‡ªå‹•çš„ã«è¿½åŠ ã•ã‚Œã¾ã™ã€‚"
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."
+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 "得られãŸä¸€é€£ã®ãƒ‡ãƒ¼ã‚¿ã‚’å°ã•ã„é †ã«ä¸¦ã¹ãŸã¨ãã«ä¸­å¤®ã«ä½ç½®ã™ã‚‹å€¤ã€‚例ãˆã°ã€3, 5, 9ã®ä¸­å¤®å€¤ã¯5。3, 5, 7, 8ã®ä¸­å¤®å€¤ã¯ (5+7)/2 = 6。"
+
+msgid "There are problems accessing Git storage: "
msgstr ""
-"得られãŸä¸€é€£ã®ãƒ‡ãƒ¼ã‚¿ã‚’å°ã•ã„é †ã«ä¸¦ã¹ãŸã¨ãã«ä¸­å¤®ã«ä½ç½®ã™ã‚‹å€¤ã€‚例ãˆã°ã€3, 5, 9ã®ä¸­å¤®å€¤ã¯5。3, 5, 7, 8ã®ä¸­å¤®å€¤ã¯ (5+7)/2 = "
-"6。"
-msgid ""
-"This means you can not push code until you create an empty repository or "
-"import existing one."
+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"
@@ -1092,6 +1342,9 @@ msgstr "ファイルをアップロード"
msgid "UploadLink|click to upload"
msgstr "クリックã—ã¦ã‚¢ãƒƒãƒ—ロード"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "全体通知設定を利用"
@@ -1116,34 +1369,22 @@ 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}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr "%{group_name} グループを削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚\n"
-"削除ã•ã‚ŒãŸã‚°ãƒ«ãƒ¼ãƒ—ã¯çµ¶å¯¾ã«å…ƒã«æˆ»ã›ã¾ã›ã‚“ï¼\n"
-"本当ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "%{group_name} グループを削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚ 削除ã•ã‚ŒãŸã‚°ãƒ«ãƒ¼ãƒ—ã¯çµ¶å¯¾ã«å…ƒã«æˆ»ã›ã¾ã›ã‚“ï¼æœ¬å½“ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"%{project_name_with_namespace} プロジェクトを削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚\n"
-"削除ã•ã‚ŒãŸãƒ—ロジェクトã¯çµ¶å¯¾ã«å…ƒã«ã¯æˆ»ã›ã¾ã›ã‚“ï¼\n"
-"本当ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "%{project_name_with_namespace} プロジェクトを削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚削除ã•ã‚ŒãŸãƒ—ロジェクトã¯çµ¶å¯¾ã«å…ƒã«ã¯æˆ»ã›ã¾ã›ã‚“ï¼æœ¬å½“ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
-msgid ""
-"You are going to remove the fork relationship to source project "
-"%{forked_from_project}. Are you ABSOLUTELY sure?"
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
msgstr "å…ƒã®ãƒ—ロジェクト (%{forked_from_project}) ã¨ã®ãƒªãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ã‚’削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚本当ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
-msgid ""
-"You are going to transfer %{project_name_with_namespace} to another owner. "
-"Are you ABSOLUTELY sure?"
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "%{project_name_with_namespace} プロジェクトを別ã®ã‚ªãƒ¼ãƒŠãƒ¼ã«ç§»è­²ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚本当ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
msgid "You can only add files when you are on a branch"
@@ -1164,28 +1405,19 @@ 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"
+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"
+msgid "You will receive notifications only for comments in which you were @mentioned"
msgstr "ã‚ãªãŸãŒ @mentioned ã§ã‚³ãƒ¡ãƒ³ãƒˆã•ã‚ŒãŸæ™‚ã®ã¿é€šçŸ¥ã—ã¾ã™"
-msgid ""
-"You won't be able to pull or push project code via %{protocol} until you "
-"%{set_password_link} on your account"
-msgstr ""
-"%{set_password_link} ã§ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ãƒ‘スワードãŒã‚»ãƒƒãƒˆã•ã‚Œã¦ã„ãªã„ã®ã§ã€ãƒ—ロジェクト㫠%{protocol} "
-"ã§ã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã‚’プッシュã€ãƒ—ルã§ãã¾ã›ã‚“"
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr "%{set_password_link} ã§ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ãƒ‘スワードãŒã‚»ãƒƒãƒˆã•ã‚Œã¦ã„ãªã„ã®ã§ã€ãƒ—ロジェクト㫠%{protocol} ã§ã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã‚’プッシュã€ãƒ—ルã§ãã¾ã›ã‚“"
-msgid ""
-"You won't be able to pull or push project code via SSH until you "
-"%{add_ssh_key_link} to your profile"
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "%{add_ssh_key_link} をプロファイルã«è¿½åŠ ã—ã¦ã„ãªã„ã®ã§ã€ãƒ—ロジェクトã«ã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã‚’プッシュã€ãƒ—ルã§ãã¾ã›ã‚“"
msgid "Your name"
@@ -1204,3 +1436,4 @@ msgstr "メール通知"
msgid "parent"
msgid_plural "parents"
msgstr[0] "親"
+
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index 97a844ada7f..4baefdb9a3e 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -1,36 +1,51 @@
-# chang-ho,cha <changho.cha@gmail.com>, 2017. #zanata
-# Huang Tao <htve@outlook.com>, 2017. #zanata
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-13 12:07-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Korean\n"
+"Language: ko_KR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-08-06 09:40-0400\n"
-"Last-Translator: chang-ho,cha <changho.cha@gmail.com>\n"
-"Language-Team: Korean (https://translate.zanata.org/project/view/GitLab)\n"
-"Language: ko\n"
-"X-Generator: Zanata 3.9.6\n"
-"Plural-Forms: nplurals=1; plural=0\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: ko\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d 커밋"
msgid "%s additional commit has been omitted to prevent performance issues."
-msgid_plural ""
-"%s additional commits have been omitted to prevent performance issues."
+msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "%s 추가 ì»¤ë°‹ì€ ì„±ëŠ¥ ì´ìŠˆë¥¼ 방지하기 위해 ìƒëžµë˜ì—ˆìŠµë‹ˆë‹¤."
msgid "%{commit_author_link} committed %{commit_timeago}"
-msgstr "%{commit_timeago} ì— %{commit_author_link} ì´(ê°€) 커밋하였습니다. "
+msgstr "%{commit_timeago} ì— %{commit_author_link} ë‹˜ì´ ì»¤ë°‹í•˜ì˜€ìŠµë‹ˆë‹¤. "
+
+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 "%{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 "%{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] ""
+
+msgid "(checkout the %{link} for information on how to install it)."
+msgstr "설치 ë°©ë²•ì— ëŒ€í•œ 정보를 얻기 위해 %{link} 를 ì²´í¬ì•„웃하세요."
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "1 파ì´í”„ë¼ì¸"
-msgstr[1] "%d 파ì´í”„ë¼ì¸"
+msgstr[0] "%d 파ì´í”„ë¼ì¸"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "지ì†ì ì¸ í†µí•©ì— ê´€í•œ 그래프 모ìŒ"
@@ -38,6 +53,18 @@ msgstr "지ì†ì ì¸ í†µí•©ì— ê´€í•œ 그래프 모ìŒ"
msgid "About auto deploy"
msgstr "ìžë™ ë°°í¬ ì •ë³´"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr "활성"
@@ -59,26 +86,99 @@ msgstr "í”„ë¡œí•„ì— SSH 키를 추가하여 SSH를 통해 Pull 하거나 Pushí•
msgid "Add new directory"
msgstr "새 디렉토리 추가"
+msgid "All"
+msgstr "ì „ì²´"
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
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 "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "드래그 &amp; 드롭 ë˜ëŠ” %{upload_link}"
+msgid "Authentication log"
+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 "Branch"
msgid_plural "Branches"
msgstr[0] "브랜치"
-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 ""
-"<strong>%{branch_name}</strong> 브랜치가 ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. ìžë™ ë°°í¬ë¥¼ 설정하려면 GitLab CI Yaml "
-"í…œí”Œë¦¿ì„ ì„ íƒí•˜ê³  변경 ì‚¬í•­ì„ ì ìš©í•˜ì‹­ì‹œì˜¤. %{link_to_autodeploy_doc}"
+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 "<strong>%{branch_name}</strong> 브랜치가 ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤. ìžë™ ë°°í¬ë¥¼ 설정하려면 GitLab CI Yaml í…œí”Œë¦¿ì„ ì„ íƒí•˜ê³  변경 ì‚¬í•­ì„ ì ìš©í•˜ì‹­ì‹œì˜¤. %{link_to_autodeploy_doc}"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "브랜치 검색"
@@ -104,12 +204,18 @@ msgstr "íŒŒì¼ ì°¾ì•„ë³´ê¸°"
msgid "ByAuthor|by"
msgstr "작성ìž"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "CI 설정"
msgid "Cancel"
msgstr "취소"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "브랜치ì—ì„œ Pick"
@@ -128,6 +234,9 @@ msgstr "변경사항"
msgid "Charts"
msgstr "차트"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "ì´ ì»¤ë°‹ì„ Cherry-pick"
@@ -188,6 +297,9 @@ msgstr "건너 뜀"
msgid "CiStatus|running"
msgstr "실행 중"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "커밋"
@@ -219,12 +331,18 @@ msgstr "커밋한 사용ìž"
msgid "Compare"
msgstr "비êµ"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "ê¸°ì—¬ì— ëŒ€í•œ 안내"
msgid "Contributors"
msgstr "기여해 주신 분들"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "URLì„ í´ë¦½ë³´ë“œì— 복사"
@@ -234,9 +352,10 @@ msgstr "ì»¤ë°‹ì˜ SHA를 í´ë¦½ë³´ë“œë¡œ 복사합니다"
msgid "Create New Directory"
msgstr "새 디렉토리 만들기"
-msgid ""
-"Create a personal access token on your account to pull or push via "
-"%{protocol}."
+msgid "Create a new branch"
+msgstr "새 브랜치 ìƒì„±"
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "%{protocol}ì„ (를) 통해 Pull 하거나 Push í•  ê°œì¸ ì•¡ì„¸ìŠ¤ 토í°ì„ 만드십시오."
msgid "Create directory"
@@ -269,20 +388,13 @@ 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 ""
-"ì‚¬ìš©ìž ì •ì˜ ì•Œë¦¼ ìˆ˜ì¤€ì€ ì°¸ì—¬ 수준과 ë™ì¼í•©ë‹ˆë‹¤. 맞춤 알림 ìˆ˜ì¤€ì„ ì‚¬ìš©í•˜ë©´ ì¼ë¶€ ì´ë²¤íŠ¸ì— 대한 ì•Œë¦¼ë„ ë°›ê²Œë©ë‹ˆë‹¤. ìžì„¸í•œ ë‚´ìš©ì€ "
-"%{notification_link}ì„ í™•ì¸í•˜ì‹­ì‹œì˜¤."
+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 "ì‚¬ìš©ìž ì •ì˜ ì•Œë¦¼ ìˆ˜ì¤€ì€ ì°¸ì—¬ 수준과 ë™ì¼í•©ë‹ˆë‹¤. 맞춤 알림 ìˆ˜ì¤€ì„ ì‚¬ìš©í•˜ë©´ ì¼ë¶€ ì´ë²¤íŠ¸ì— 대한 ì•Œë¦¼ë„ ë°›ê²Œë©ë‹ˆë‹¤. ìžì„¸í•œ ë‚´ìš©ì€ %{notification_link}ì„ í™•ì¸í•˜ì‹­ì‹œì˜¤."
msgid "Cycle Analytics"
-msgstr "Cycle Analytics"
+msgstr ""
-msgid ""
-"Cycle Analytics gives an overview of how much time it takes to go from idea "
-"to production in your project."
+msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr "Cycle Analytics는 프로ì íŠ¸ì—ì„œ ì•„ì´ë””어를 프로ë•ì…˜ìœ¼ë¡œ 옮기는 ë° ê±¸ë¦¬ëŠ” ì‹œê°„ì„ ëŒ€ëžµì ìœ¼ë¡œ ë³´ì—¬ì¤ë‹ˆë‹¤."
msgid "CycleAnalyticsStage|Code"
@@ -316,12 +428,21 @@ msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "ë°°í¬"
+msgid "Deploy Keys"
+msgstr ""
+
msgid "Description"
msgstr "설명"
+msgid "Details"
+msgstr "ìƒì„¸"
+
msgid "Directory name"
msgstr "디렉토리 ì´ë¦„"
+msgid "Discard changes"
+msgstr "변경 내용 취소"
+
msgid "Don't show again"
msgstr "다시 표시하지 ì•ŠìŒ"
@@ -358,6 +479,27 @@ msgstr "편집"
msgid "Edit Pipeline Schedule %{id}"
msgstr "파ì´í”„ë¼ì¸ 스케줄 편집 %{id}"
+msgid "Emails"
+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 "ë§¤ì¼ (오전 4ì‹œì—)"
@@ -404,35 +546,71 @@ 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 "git storage ìƒíƒœ ì •ë³´ê°€ 초기화ë˜ì—ˆìŠµë‹ˆë‹¤."
+
+msgid "GitLab Runner section"
+msgstr "GitLab Runner 섹션"
+
msgid "Go to your fork"
msgstr "ë‹¹ì‹ ì˜ í¬í¬ë¡œ ì´ë™í•˜ì„¸ìš”"
msgid "GoToYourFork|Fork"
msgstr "í¬í¬"
+msgid "Group overview"
+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 "Home"
msgstr "홈"
+msgid "Hooks"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "Housekeepingì´ ì„±ê³µì ìœ¼ë¡œ 시작ë˜ì—ˆìŠµë‹ˆë‹¤"
msgid "Import repository"
msgstr "저장소 가져 오기"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr "GitLab CI 와 호환ë˜ëŠ” Runner 설치"
+
msgid "Interval Pattern"
msgstr "주기 패턴"
msgid "Introducing Cycle Analytics"
msgstr "Cycle Analytics 소개"
-msgid "Jobs for last month"
-msgstr "지난달 Jobs"
-
-msgid "Jobs for last week"
-msgstr "지난주 Jobs"
+msgid "Issue events"
+msgstr "ì´ìŠˆ ì´ë²¤íŠ¸"
-msgid "Jobs for last year"
-msgstr "지난해 Jobs"
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "Disabled"
@@ -440,6 +618,9 @@ msgstr "Disabled"
msgid "LFSStatus|Enabled"
msgstr "Enabled"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "최근 %d ì¼"
@@ -448,11 +629,17 @@ msgid "Last Pipeline"
msgstr "최근 파ì´í”„ë¼ì¸"
msgid "Last Update"
-msgstr "최근 ì—…ë°ì´íŠ¸:"
+msgstr "최근 ì—…ë°ì´íŠ¸"
msgid "Last commit"
msgstr "최근 커밋"
+msgid "LastPushEvent|You pushed to"
+msgstr "푸쉬: "
+
+msgid "LastPushEvent|at"
+msgstr "at"
+
msgid "Learn more in the"
msgstr "ë” ìžì„¸ížˆ 알아보기"
@@ -465,16 +652,40 @@ 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] "최대 %d ì´ë²¤íŠ¸ 만 표시하는 것으로 제한ë©ë‹ˆë‹¤."
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "중앙값"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr "머지 ì´ë²¤íŠ¸"
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "SSH 키 추가"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr "여기"
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "새 ì´ìŠˆ"
@@ -555,7 +766,7 @@ msgid "NotificationEvent|Successful pipeline"
msgstr "성공ì ì¸ 파ì´í”„ë¼ì¸"
msgid "NotificationLevel|Custom"
-msgstr "커스텀"
+msgstr "ì‚¬ìš©ìž ì •ì˜"
msgid "NotificationLevel|Disabled"
msgstr "사용 안 함"
@@ -572,6 +783,9 @@ msgstr "참여"
msgid "NotificationLevel|Watch"
msgstr "Watch"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "í•„í„°"
@@ -581,9 +795,15 @@ msgstr "열린"
msgid "Options"
msgstr "옵션 "
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
msgstr "소유ìž"
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
msgstr "파ì´í”„ë¼ì¸"
@@ -596,6 +816,9 @@ msgstr "파ì´í”„ë¼ì¸ 스케쥴"
msgid "Pipeline Schedules"
msgstr "파ì´í”„ë¼ì¸ 스케쥴"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "실패 :"
@@ -659,6 +882,15 @@ 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 "모ë‘"
@@ -671,6 +903,15 @@ msgstr "스테ì´ì§•"
msgid "Pipeline|with stages"
msgstr "스테ì´ì§•"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "'%{project_name}'프로ì íŠ¸ê°€ ì‚­ì œ 처리 중입니다."
@@ -686,15 +927,16 @@ msgstr "'%{project_name}'프로ì íŠ¸ê°€ ì‚­ì œë©ë‹ˆë‹¤."
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."
+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."
@@ -703,6 +945,12 @@ msgstr "프로ì íŠ¸ 내보내기가 시작ë˜ì—ˆìŠµë‹ˆë‹¤. 다운로드 ë§í¬ë
msgid "Project home"
msgstr "프로ì íŠ¸ 홈"
+msgid "Project overview"
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr "구ë…"
+
msgid "ProjectFeature|Disabled"
msgstr "사용 안 함"
@@ -724,11 +972,17 @@ msgstr "스테ì´ì§•"
msgid "ProjectNetworkGraph|Graph"
msgstr "그래프"
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr "푸쉬 ì´ë²¤íŠ¸"
+
msgid "Read more"
msgstr "ë” ì½ê¸°"
msgid "Readme"
-msgstr "Readme"
+msgstr ""
msgid "RefSwitcher|Branches"
msgstr "브랜치"
@@ -760,15 +1014,30 @@ msgstr "ë‚˜ì¤‘ì— ë‹¤ì‹œ 알림"
msgid "Remove project"
msgstr "프로ì íŠ¸ ì‚­ì œ"
+msgid "Repository"
+msgstr ""
+
msgid "Request Access"
msgstr "액세스 요청"
+msgid "Reset git storage health information"
+msgstr "git storage 헬스 정보 초기화"
+
+msgid "Reset health check access token"
+msgstr "헬스 ì²´í¬ ì ‘ê·¼ í† í° ì´ˆê¸°í™”"
+
+msgid "Reset runners registration token"
+msgstr "runner ë“±ë¡ í† í° ì´ˆê¸°í™”"
+
msgid "Revert this commit"
msgstr "ì´ ì»¤ë°‹ ë˜ëŒë¦¬ê¸°"
msgid "Revert this merge request"
msgstr "ì´ ë¨¸ì§€ 리퀘스트 ë˜ëŒë¦¬ê¸°"
+msgid "SSH Keys"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "파ì´í”„ë¼ì¸ 스케줄 저장"
@@ -787,11 +1056,17 @@ 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 "%{protocol}ì„(를) 통해 Pull 하거나 Push하려면 ê³„ì •ì— íŒ¨ìŠ¤ì›Œë“œë¥¼ 설정하십시오."
+msgstr "%{protocol} í”„ë¡œí† ì½œì„ í†µí•´ Pull 하거나 Push하려면 ê³„ì •ì— íŒ¨ìŠ¤ì›Œë“œë¥¼ 설정하십시오."
msgid "Set up CI"
msgstr "CI 설정"
@@ -805,19 +1080,34 @@ msgstr "ìžë™ ë°°í¬ ì„¤ì •"
msgid "SetPasswordToCloneLink|set a password"
msgstr "패스워드 설정"
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "%d ê°œì˜ ì´ë²¤íŠ¸ 표시 중"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "소스 코드"
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr "Runner 설정 중 ë‹¤ìŒ URLì„ ì§€ì •í•˜ì„¸ìš”."
+
msgid "StarProject|Star"
msgstr "별표"
msgid "Start a %{new_merge_request} with these changes"
msgstr "ì´ ë³€ê²½ 사항으로 %{new_merge_request} ì„ ì‹œìž‘í•˜ì‹­ì‹œì˜¤."
+msgid "Start the Runner!"
+msgstr "Runner 시작!"
+
msgid "Switch branch/tag"
msgstr "스위치 브랜치/태그"
@@ -831,13 +1121,11 @@ msgstr "태그 "
msgid "Target Branch"
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."
+msgid "Team"
msgstr ""
-"Coding Stage는 첫 번째 커밋ì—서부터 머지 리퀘스트 ìƒì„±ê¹Œì§€ì˜ ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. 첫 번째 머지 ë¦¬í€˜ìŠ¤íŠ¸ì„ ìƒì„±í•˜ë©´ ë°ì´í„°ê°€ "
-"ìžë™ìœ¼ë¡œ ì—¬ê¸°ì— ì¶”ê°€ë©ë‹ˆë‹¤."
+
+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 "Coding Stage는 첫 번째 커밋ì—서부터 머지 리퀘스트 ìƒì„±ê¹Œì§€ì˜ ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. 첫 번째 머지 ë¦¬í€˜ìŠ¤íŠ¸ì„ ìƒì„±í•˜ë©´ ë°ì´í„°ê°€ ìžë™ìœ¼ë¡œ ì—¬ê¸°ì— ì¶”ê°€ë©ë‹ˆë‹¤."
msgid "The collection of events added to the data gathered for that stage."
msgstr "해당 단계ì—ì„œ 수집 ëœ ë°ì´í„°ê°€ ì´ë²¤íŠ¸ 모ìŒì— 추가ë˜ì—ˆìŠµë‹ˆë‹¤."
@@ -845,38 +1133,20 @@ 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 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 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."
+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 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 "ì´ í”„ë¡œì íŠ¸ëŠ” ë¡œê·¸ì¸ í•œ 사용ìžê°€ë§Œ 액세스 í•  수 있습니다."
@@ -887,44 +1157,25 @@ 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 ""
-"Review 단계ì—서는 머지 리퀘스트를 작성한 후 ë¨¸ì§€í•˜ê¸°ê¹Œì§€ì˜ ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. ë°ì´í„°ëŠ” 첫 번째 머지 ë¦¬í€˜ìŠ¤íŠ¸ì„ ë¨¸ì§€ í•œ í›„ì— "
-"ìžë™ìœ¼ë¡œ 추가ë©ë‹ˆë‹¤."
+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 "Review 단계ì—서는 머지 리퀘스트를 작성한 후 ë¨¸ì§€í•˜ê¸°ê¹Œì§€ì˜ ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. ë°ì´í„°ëŠ” 첫 번째 머지 ë¦¬í€˜ìŠ¤íŠ¸ì„ ë¨¸ì§€ í•œ í›„ì— ìžë™ìœ¼ë¡œ 추가ë©ë‹ˆë‹¤."
-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 ""
-"Staging 단계ì—서는 MR 머지과 프로ë•ì…˜ í™˜ê²½ì— ì½”ë“œ ë°°í¬ ì‚¬ì´ì˜ ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. ë°ì´í„°ë¥¼ Production í™˜ê²½ì— ì²˜ìŒ "
-"ë°°í¬í•˜ë©´ ë°ì´í„°ê°€ ìžë™ìœ¼ë¡œ 추가ë©ë‹ˆë‹¤."
+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 "Staging 단계ì—서는 MR 머지과 프로ë•ì…˜ í™˜ê²½ì— ì½”ë“œ ë°°í¬ ì‚¬ì´ì˜ ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. ë°ì´í„°ë¥¼ Production í™˜ê²½ì— ì²˜ìŒ ë°°í¬í•˜ë©´ ë°ì´í„°ê°€ ìžë™ìœ¼ë¡œ 추가ë©ë‹ˆë‹¤."
-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 ""
-"테스트 단계ì—서는 GitLab CIê°€ 관련 머지 ë¦¬í€˜ìŠ¤íŠ¸ì„ ìœ„í•´ 모든 파ì´í”„ë¼ì¸ì„ 실행하는 ë° ê±¸ë¦¬ëŠ” ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. 첫 번째 "
-"파ì´í”„ë¼ì¸ ì‹¤í–‰ì´ ì™„ë£Œë˜ë©´ ë°ì´í„°ê°€ ìžë™ìœ¼ë¡œ 추가ë©ë‹ˆë‹¤."
+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 "테스트 단계ì—서는 GitLab CIê°€ 관련 머지 ë¦¬í€˜ìŠ¤íŠ¸ì„ ìœ„í•´ 모든 파ì´í”„ë¼ì¸ì„ 실행하는 ë° ê±¸ë¦¬ëŠ” ì‹œê°„ì„ ë³´ì—¬ì¤ë‹ˆë‹¤. 첫 번째 파ì´í”„ë¼ì¸ ì‹¤í–‰ì´ ì™„ë£Œë˜ë©´ ë°ì´í„°ê°€ ìžë™ìœ¼ë¡œ 추가ë©ë‹ˆë‹¤."
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 ""
-"ê°’ì€ ì¼ë ¨ì˜ 관측 ê°’ 중ì ì— 있습니다. 예를 들어, 3, 5, 9 사ì´ì˜ 중간 ê°’ì€ 5입니다. 3, 5, 7, 8 사ì´ì˜ 중간 ê°’ì€ (5 "
-"+ 7) / 2 = 6입니다."
+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 "ê°’ì€ ì¼ë ¨ì˜ 관측 ê°’ 중ì ì— 있습니다. 예를 들어, 3, 5, 9 사ì´ì˜ 중간 ê°’ì€ 5입니다. 3, 5, 7, 8 사ì´ì˜ 중간 ê°’ì€ (5 + 7) / 2 = 6입니다."
-msgid ""
-"This means you can not push code until you create an empty repository or "
-"import existing one."
+msgid "There are problems accessing Git storage: "
+msgstr "git storageì— ì ‘ê·¼í•˜ëŠ”ë° ë¬¸ì œê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤. "
+
+msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "즉, 빈 저장소를 만들거나 기존 저장소를 가져올 때까지 코드를 Push 할 수 없습니다."
msgid "Time before an issue gets scheduled"
@@ -1091,6 +1342,9 @@ msgstr "íŒŒì¼ ì—…ë¡œë“œ"
msgid "UploadLink|click to upload"
msgstr "업로드하려면 í´ë¦­í•˜ì‹­ì‹œì˜¤."
+msgid "Use the following registration token during setup:"
+msgstr "설정 ì¤‘ì— ë‹¤ìŒ ë“±ë¡ í† í° ì´ìš© : "
+
msgid "Use your global notification setting"
msgstr "전체 알림 설정 사용"
@@ -1115,33 +1369,22 @@ 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}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr "%{group_name}ì„(를) 제거하려고합니다.\n"
-"\"ì •ë§ë¡œ\" 확실합니까?"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "%{group_name} ê·¸ë£¹ì„ ì œê±°í•˜ë ¤ê³ í•©ë‹ˆë‹¤. \\\"ì •ë§ë¡œ\\\" 확실합니까?"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"%{project_name_with_namespace}ì„(를) 삭제하려고합니다.\n"
-"ì‚­ì œëœ í”„ë¡œì íŠ¸ë¥¼ ë³µì› í•  수 없습니다!\n"
-"\"ì •ë§ë¡œ\" 확실합니까?"
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "%{project_name_with_namespace} 프로ì íŠ¸ë¥¼ 삭제하려고합니다. \"ì‚­ì œëœ í”„ë¡œì íŠ¸ë¥¼ ë³µì› í•  수 없습니다! \\\"ì •ë§ë¡œ\\\" 확실합니까?"
-msgid ""
-"You are going to remove the fork relationship to source project "
-"%{forked_from_project}. Are you ABSOLUTELY sure?"
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
msgstr "í¬í¬ 관계를 소스 프로ì íŠ¸ %{forked_from_project}ì— ëŒ€í•´ 제거하려고합니다. \"ì •ë§ë¡œ\" 확실합니까?"
-msgid ""
-"You are going to transfer %{project_name_with_namespace} to another owner. "
-"Are you ABSOLUTELY sure?"
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "%{project_name_with_namespace}ì„ ë‹¤ë¥¸ 소유ìžì—게 ì´ì „하려고합니다. \"ì •ë§ë¡œ\" 확실합니까?"
msgid "You can only add files when you are on a branch"
@@ -1162,31 +1405,20 @@ 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"
+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"
+msgid "You will receive notifications only for comments in which you were @mentioned"
msgstr "ë‹¹ì‹ ì€ ë‹¹ì‹ ì´ @mentioned í•œ ì½”ë©˜íŠ¸ì— ëŒ€í•´ì„œë§Œ 통지를 받게ë©ë‹ˆë‹¤."
-msgid ""
-"You won't be able to pull or push project code via %{protocol} until you "
-"%{set_password_link} on your account"
-msgstr ""
-"ë‹¹ì‹ ì˜ ê³„ì •ì— %{set_password_link} ì„ í•˜ê¸° ì „ì—는 %{protocol} í”„ë¡œí† ì½œì„ í†µí•´ 프로ì íŠ¸ 코드를 Pull 하거나 "
-"Push 할 수 없습니다"
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr "ë‹¹ì‹ ì˜ ê³„ì •ì— %{set_password_link} ì„ í•˜ê¸° ì „ì—는 %{protocol} í”„ë¡œí† ì½œì„ í†µí•´ 프로ì íŠ¸ 코드를 Pull 하거나 Push í•  수 없습니다"
-msgid ""
-"You won't be able to pull or push project code via SSH until you "
-"%{add_ssh_key_link} to your profile"
-msgstr ""
-"ë‹¹ì‹ ì˜ í”„ë¡œí•„ì— %{add_ssh_key_link} ì„(를) 하기 ì „ì—는 SSH를 통해 프로ì íŠ¸ 코드를 Pull 하거나 Push í•  수 "
-"없습니다"
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr "ë‹¹ì‹ ì˜ í”„ë¡œí•„ì— %{add_ssh_key_link} 를 하기 ì „ì—는 SSH를 통해 프로ì íŠ¸ 코드를 Pull 하거나 Push í•  수 없습니다"
msgid "Your name"
msgstr "ê·€í•˜ì˜ ì´ë¦„"
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index 2eaadb64124..88ca25dbb3b 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -1,42 +1,55 @@
-# Alexandre Alencar <alexandre.alencar@gmail.com>, 2017. #zanata
-# Fabio Beneditto <fabiobeneditto@gmail.com>, 2017. #zanata
-# Leandro Nunes dos Santos <leandronunes@gmail.com>, 2017. #zanata
-# Huang Tao <htve@outlook.com>, 2017. #zanata
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-13 12:07-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Portuguese, Brazilian\n"
+"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Language-Team: Portuguese (Brazil) (https://translate.zanata.org/project/view/GitLab)\n"
-"PO-Revision-Date: 2017-08-03 11:29-0400\n"
-"Last-Translator: Alexandre Alencar <alexandre.alencar@gmail.com>\n"
-"Language: pt-BR\n"
-"X-Generator: Zanata 3.9.6\n"
-"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: pt-BR\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
-msgid "%s additional commit has been omitted to prevent performance issues."
-msgid_plural ""
-"%s additional commits have been omitted to prevent performance issues."
+msgid "%d commit"
+msgid_plural "%d commits"
msgstr[0] ""
-"%s commit adicional foi omitido para prevenir problemas de performance."
msgstr[1] ""
-"%s commits adicionais foram omitidos para prevenir problemas de performance."
-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 commit adicional foi omitido para prevenir problemas de performance."
+msgstr[1] "%s commits adicionais foram omitidos para prevenir problemas de performance."
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "%{commit_author_link} fez commit %{commit_timeago}"
+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 ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "1 pipeline"
-msgstr[1] "%d pipelines"
+msgstr[0] ""
+msgstr[1] ""
msgid "A collection of graphs regarding Continuous Integration"
msgstr "Uma coleção de gráficos sobre Integração Contínua"
@@ -44,6 +57,18 @@ msgstr "Uma coleção de gráficos sobre Integração Contínua"
msgid "About auto deploy"
msgstr "Sobre o deploy automático"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr "Ativo"
@@ -65,28 +90,100 @@ msgstr "Adicionar chave SSH ao seu perfil para fazer pull ou push via SSH."
msgid "Add new directory"
msgstr "Adicionar novo diretório"
+msgid "All"
+msgstr ""
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "Projeto arquivado! O repositório é somente leitura"
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "Tem certeza que deseja excluir este agendamento de pipeline?"
+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 "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Para anexar arquivo, arraste e solte ou %{upload_link}"
+msgid "Authentication log"
+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 "Branch"
msgid_plural "Branches"
-msgstr[0] "Branch"
-msgstr[1] "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 ""
-"O branch <strong>%{branch_name}</strong> foi criado. Para configurar o "
-"deploy automático, selecione um modelo de Yaml do GitLab CI e commit suas "
-"mudanças. %{link_to_autodeploy_doc}"
+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 "O branch <strong>%{branch_name}</strong> foi criado. Para configurar o deploy automático, selecione um modelo de Yaml do GitLab CI e commit suas mudanças. %{link_to_autodeploy_doc}"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Procurar por branches"
@@ -95,7 +192,7 @@ msgid "BranchSwitcherTitle|Switch branch"
msgstr "Mudar de branch"
msgid "Branches"
-msgstr "Branches"
+msgstr ""
msgid "Browse Directory"
msgstr "Navegar no Diretório"
@@ -112,12 +209,18 @@ msgstr "Navegar pelos arquivos"
msgid "ByAuthor|by"
msgstr "por"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "Configuração da IC"
msgid "Cancel"
msgstr "Cancelar"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Pick para um branch"
@@ -136,6 +239,9 @@ msgstr "Registro de mudanças"
msgid "Charts"
msgstr "Gráficos"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Cherry-pick esse commit"
@@ -196,10 +302,13 @@ msgstr "ignorado"
msgid "CiStatus|running"
msgstr "executando"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
-msgstr[0] "Commit"
-msgstr[1] "Commits"
+msgstr[0] ""
+msgstr[1] ""
msgid "Commit duration in minutes for last 30 commits"
msgstr "Duração do commit em minutos para os últimos 30 commits"
@@ -214,7 +323,7 @@ msgid "CommitMessage|Add %{file_name}"
msgstr "Adicionar %{file_name}"
msgid "Commits"
-msgstr "Commits"
+msgstr ""
msgid "Commits feed"
msgstr "Feed de commits"
@@ -228,12 +337,18 @@ msgstr "Commit feito por"
msgid "Compare"
msgstr "Comparar"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "Guia de contribuição"
msgid "Contributors"
msgstr "Contribuidores"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "Copiar URL para área de transferência"
@@ -243,12 +358,11 @@ msgstr "Copiar SHA do commit para a área de transferência"
msgid "Create New Directory"
msgstr "Criar Novo Diretório"
-msgid ""
-"Create a personal access token on your account to pull or push via "
-"%{protocol}."
+msgid "Create a new branch"
msgstr ""
-"Crie um token de acesso pessoal na sua conta para dar pull ou push via "
-"%{protocol}."
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr "Crie um token de acesso pessoal na sua conta para dar pull ou push via %{protocol}."
msgid "Create directory"
msgstr "Criar diretório"
@@ -280,25 +394,14 @@ msgstr "Sintaxe do cron"
msgid "Custom notification events"
msgstr "Eventos de notificação personalizados"
-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 ""
-"Níveis de notificação personalizados são equivalentes a níveis de "
-"participação. Com níveis de notificação personalizados você também será "
-"notificado sobre eventos selecionados. Para mais informações, visite "
-"%{notification_link}."
+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 "Níveis de notificação personalizados são equivalentes a níveis de participação. Com níveis de notificação personalizados você também será notificado sobre eventos selecionados. Para mais informações, visite %{notification_link}."
msgid "Cycle Analytics"
msgstr "Análise de Ciclo"
-msgid ""
-"Cycle Analytics gives an overview of how much time it takes to go from idea "
-"to production in your project."
-msgstr ""
-"A Análise de Ciclo fornece uma visão geral de quanto tempo uma ideia demora "
-"para ir para produção em seu projeto."
+msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
+msgstr "A Análise de Ciclo fornece uma visão geral de quanto tempo uma ideia demora para ir para produção em seu projeto."
msgid "CycleAnalyticsStage|Code"
msgstr "Código"
@@ -332,12 +435,21 @@ msgid_plural "Deploys"
msgstr[0] "Implantação"
msgstr[1] "Implantações"
+msgid "Deploy Keys"
+msgstr ""
+
msgid "Description"
msgstr "Descrição"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "Nome do diretório"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "Não exibir novamente"
@@ -374,6 +486,27 @@ msgstr "Alterar"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Alterar Agendamento do Pipeline %{id}"
+msgid "Emails"
+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 "Todos os dias (às 4:00)"
@@ -409,8 +542,8 @@ msgstr "publicado por"
msgid "Fork"
msgid_plural "Forks"
-msgstr[0] "Fork"
-msgstr[1] "Forks"
+msgstr[0] ""
+msgstr[1] ""
msgid "ForkedFromProjectPath|Forked from"
msgstr "Fork criado a partir de"
@@ -421,35 +554,71 @@ msgstr "Da abertura de tarefas até a implantação para a produção"
msgid "From merge request merge until deploy to production"
msgstr "Do merge request até a implantação em produção"
+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 "Ir para seu fork"
msgid "GoToYourFork|Fork"
msgstr "Fork"
+msgid "Group overview"
+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 "Home"
msgstr "Início"
+msgid "Hooks"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "Manutenção iniciada com sucesso"
msgid "Import repository"
msgstr "Importar repositório"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Padrão de intervalo"
msgid "Introducing Cycle Analytics"
msgstr "Apresentando a Análise de Ciclo"
-msgid "Jobs for last month"
-msgstr "Jobs no último mês"
-
-msgid "Jobs for last week"
-msgstr "Jobs na última semana"
+msgid "Issue events"
+msgstr ""
-msgid "Jobs for last year"
-msgstr "Jobs no último ano"
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "Desabilitado"
@@ -457,6 +626,9 @@ msgstr "Desabilitado"
msgid "LFSStatus|Enabled"
msgstr "Habilitado"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "Último %d dia"
@@ -471,6 +643,12 @@ msgstr "Última Atualização"
msgid "Last commit"
msgstr "Último commit"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "Saiba mais em"
@@ -483,17 +661,41 @@ msgstr "Sair do grupo"
msgid "Leave project"
msgstr "Sair do projeto"
+msgid "License"
+msgstr ""
+
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "Limitado a mostrar %d evento, no máximo"
msgstr[1] "Limitado a mostrar %d eventos, no máximo"
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "Mediana"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "adicione uma chave SSH"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nova Issue"
@@ -592,6 +794,9 @@ msgstr "Participar"
msgid "NotificationLevel|Watch"
msgstr "Observar"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Filtrar"
@@ -601,11 +806,17 @@ msgstr "Aberto"
msgid "Options"
msgstr "Opções"
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
msgstr "Proprietário"
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
-msgstr "Pipeline"
+msgstr ""
msgid "Pipeline Health"
msgstr "Saúde da Pipeline"
@@ -616,6 +827,9 @@ msgstr "Agendamento da Pipeline"
msgid "Pipeline Schedules"
msgstr "Agendamentos da Pipeline"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Falhou:"
@@ -674,11 +888,20 @@ msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "Personalizado"
msgid "Pipelines"
-msgstr "Pipelines"
+msgstr ""
msgid "Pipelines charts"
msgstr "Gráficos de pipelines"
+msgid "Pipelines for last month"
+msgstr ""
+
+msgid "Pipelines for last week"
+msgstr ""
+
+msgid "Pipelines for last year"
+msgstr ""
+
msgid "Pipeline|all"
msgstr "todos"
@@ -691,6 +914,15 @@ msgstr "com etapa"
msgid "Pipeline|with stages"
msgstr "com etapas"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Projeto'%{project_name}' marcado para exclusão."
@@ -704,8 +936,10 @@ msgid "Project '%{project_name}' will be deleted."
msgstr "Projeto '%{project_name}' será excluído."
msgid "Project access must be granted explicitly to each user."
+msgstr "Acesso ao projeto deve ser concedido explicitamente para cada usuário."
+
+msgid "Project details"
msgstr ""
-"Acesso ao projeto deve ser concedido explicitamente para cada usuário."
msgid "Project export could not be deleted."
msgstr "A exportação do projeto não pôde ser excluída."
@@ -713,21 +947,21 @@ msgstr "A exportação do projeto não pôde ser excluída."
msgid "Project export has been deleted."
msgstr "Exportação do projeto excluída."
-msgid ""
-"Project export link has expired. Please generate a new export from your "
-"project settings."
-msgstr ""
-"O link para a exportação do projeto expirou. Favor gerar uma nova exportação "
-"a partir das configurações do projeto."
+msgid "Project export link has expired. Please generate a new export from your project settings."
+msgstr "O link para a exportação do projeto expirou. Favor gerar uma nova exportação a partir das configurações do projeto."
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."
-""
+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 ""
+
msgid "ProjectFeature|Disabled"
msgstr "Desabilitado"
@@ -749,6 +983,12 @@ msgstr "Etapa"
msgid "ProjectNetworkGraph|Graph"
msgstr "Ãrvore"
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Leia mais"
@@ -785,15 +1025,30 @@ msgstr "Lembrar mais tarde"
msgid "Remove project"
msgstr "Remover projeto"
+msgid "Repository"
+msgstr ""
+
msgid "Request Access"
msgstr "Solicitar acesso"
+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 "Reverter este commit"
msgid "Revert this merge request"
msgstr "Reverter esse merge request"
+msgid "SSH Keys"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "Salvar agendamento da pipeline"
@@ -812,13 +1067,17 @@ msgstr "Selecionar Formato do Arquivo"
msgid "Select a timezone"
msgstr "Selecionar fuso horário"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "Selecionar branch de destino"
-msgid "Set a password on your account to pull or push via %{protocol}."
+msgid "Service Templates"
msgstr ""
-"Defina uma senha para sua conta para aceitar ou entregar código via "
-"%{protocol}."
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr "Defina uma senha para sua conta para aceitar ou entregar código via %{protocol}."
msgid "Set up CI"
msgstr "Configurar CI"
@@ -832,42 +1091,54 @@ msgstr "Configurar implantação automática"
msgid "SetPasswordToCloneLink|set a password"
msgstr "defina uma senha"
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Mostrando %d evento"
msgstr[1] "Mostrando %d eventos"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "Código-fonte"
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
msgid "StarProject|Star"
msgstr "Marcar"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Iniciar um %{new_merge_request} a partir dessas alterações"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Trocar branch/tag"
msgid "Tag"
msgid_plural "Tags"
-msgstr[0] "Tag"
-msgstr[1] "Tags"
+msgstr[0] ""
+msgstr[1] ""
msgid "Tags"
-msgstr "Tags"
+msgstr ""
msgid "Target Branch"
msgstr "Branch de destino"
-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."
+msgid "Team"
msgstr ""
-"A etapa de codificação mostra o tempo desde a entrega do primeiro commit até "
-"a criação do merge request. Os dados serão automaticamente adicionados aqui "
-"desde o momento de criação do merge request."
+
+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 "A etapa de codificação mostra o tempo desde a entrega do primeiro commit até a criação do merge request. Os dados serão automaticamente adicionados aqui desde o momento de criação do merge request."
msgid "The collection of events added to the data gathered for that stage."
msgstr "A coleção de eventos adicionados aos dados coletados para essa etapa."
@@ -875,44 +1146,20 @@ msgstr "A coleção de eventos adicionados aos dados coletados para essa etapa."
msgid "The fork relationship has been removed."
msgstr "O relacionamento como fork foi removido."
-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 ""
-"A etapa de planejamento mostra o tempo que se leva desde a criação de uma "
-"issue até sua atribuição à um milestone, ou sua adição a uma lista no seu "
-"Issue Board. Comece a criar issues para ver dados para esta etapa."
+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 "A etapa de planejamento mostra o tempo que se leva desde a criação de uma issue até sua atribuição à um milestone, ou sua adição a uma lista no seu Issue Board. Comece a criar issues para ver dados para esta etapa."
msgid "The phase of the development lifecycle."
msgstr "A fase do ciclo de vida do desenvolvimento."
-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 ""
-"O agendamento de pipeline executa pipelines no futuro, repetidamente, para "
-"branches ou tags específicas. Essas pipelines agendadas terão acesso "
-"limitado ao projeto baseado no seu usuário associado."
+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 "O agendamento de pipeline executa pipelines no futuro, repetidamente, para branches ou tags específicas. Essas pipelines agendadas terão acesso limitado ao projeto baseado no seu usuário associado."
-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 ""
-"A etapa de planejamento mostra o tempo do passo anterior até a publicação de "
-"seu primeiro conjunto de mudanças. Este tempo será adicionado "
-"automaticamente assim que você enviar seu primeiro conjunto de mudanças."
+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 "A etapa de planejamento mostra o tempo do passo anterior até a publicação de seu primeiro conjunto de mudanças. Este tempo será adicionado automaticamente assim que você enviar seu primeiro conjunto de mudanças."
-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 ""
-"A etapa de produção mostra o tempo total que leva entre criar uma issue e "
-"implantar o código em produção. Os dados serão adicionados automaticamente "
-"assim que você completar todo o ciclo de produção."
+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 "A etapa de produção mostra o tempo total que leva entre criar uma issue e implantar o código em produção. Os dados serão adicionados automaticamente assim que você completar todo o ciclo de produção."
msgid "The project can be accessed by any logged in user."
msgstr "O projeto pode ser acessado por qualquer usuário autenticado."
@@ -923,51 +1170,26 @@ msgstr "O projeto pode ser acessado sem a necessidade de autenticação."
msgid "The repository for this project does not exist."
msgstr "Não existe repositório para este projeto."
-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 ""
-"A etapa de revisão mostra o tempo de criação de uma solicitação de "
-"incorporação até sua aceitação. Os dados serão automaticamente adicionados "
-"depois que sua primeira solicitação de incorporação for aceita."
+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 "A etapa de revisão mostra o tempo de criação de uma solicitação de incorporação até sua aceitação. Os dados serão automaticamente adicionados depois que sua primeira solicitação de incorporação for aceita."
-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 ""
-"A etapa de homologação mostra o tempo entre o aceite da solicitação de "
-"incorporação e a implantação do código no ambiente de produção. Os dados "
-"serão automaticamente adicionados depois que você implantar em produção pela "
-"primeira vez."
+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 "A etapa de homologação mostra o tempo entre o aceite da solicitação de incorporação e a implantação do código no ambiente de produção. Os dados serão automaticamente adicionados depois que você implantar em produção pela primeira vez."
-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 ""
-"A etapa de testes mostra o tempo que o GitLab CI leva para executar cada "
-"pipeline para a solicitação de incorporação associada. Os dados serão "
-"automaticamente adicionados após a conclusão do primeiro pipeline."
+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 "A etapa de testes mostra o tempo que o GitLab CI leva para executar cada pipeline para a solicitação de incorporação associada. Os dados serão automaticamente adicionados após a conclusão do primeiro pipeline."
msgid "The time taken by each data entry gathered by that stage."
msgstr "O tempo necessário por cada entrada de dados reunida por essa etapa."
-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 ""
-"O valor situado no ponto médio de uma série de valores observados. Ex., "
-"entre 3, 5, 9, a mediana é 5. Entre 3, 5, 7, 8, a mediana é (5+7)/2 = 6."
+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 "O valor situado no ponto médio de uma série de valores observados. Ex., entre 3, 5, 9, a mediana é 5. Entre 3, 5, 7, 8, a mediana é (5+7)/2 = 6."
-msgid ""
-"This means you can not push code until you create an empty repository or "
-"import existing one."
+msgid "There are problems accessing Git storage: "
msgstr ""
-"Isto significa que você não pode entregar código até que crie um repositório "
-"vazio ou importe um existente."
+
+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."
msgid "Time before an issue gets scheduled"
msgstr "Tempo até que uma issue seja agendada"
@@ -976,9 +1198,7 @@ msgid "Time before an issue starts implementation"
msgstr "Tempo até que uma issue comece a ser implementado"
msgid "Time between merge request creation and merge/close"
-msgstr ""
-"Tempo entre a criação da solicitação de incorporação e a aceitação/"
-"fechamento"
+msgstr "Tempo entre a criação da solicitação de incorporação e a aceitação/fechamento"
msgid "Time until first merge request"
msgstr "Tempo até a primeira solicitação de incorporação"
@@ -1137,6 +1357,9 @@ msgstr "Enviar arquivo"
msgid "UploadLink|click to upload"
msgstr "clique para fazer upload"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "Utilizar configuração de notificação global"
@@ -1161,40 +1384,23 @@ msgstr "Precisa visualizar os dados? Solicite acesso ao administrador."
msgid "We don't have enough data to show this stage."
msgstr "Esta etapa não possui dados suficientes para exibição."
+msgid "Wiki"
+msgstr ""
+
msgid "Withdraw Access Request"
msgstr "Remover Requisição de Acesso"
-msgid ""
-"You are going to remove %{group_name}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Você vai remover %{group_name}.\n"
-"Grupos removidos NÃO PODEM ser restaurados!\n"
-"Você está ABSOLUTAMENTE certo?"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Você vai remover %{group_name}. Grupos removidos NÃO PODEM ser restaurados! Você está ABSOLUTAMENTE certo?"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Você irá remover %{project_name_with_namespace}.\n"
-"O projeto removido NÃO PODE ser restaurado!\n"
-"Tem certeza ABSOLUTA?"
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Você irá remover %{project_name_with_namespace}. O projeto removido NÃO PODE ser restaurado! Tem certeza ABSOLUTA?"
-msgid ""
-"You are going to remove the fork relationship to source project "
-"%{forked_from_project}. Are you ABSOLUTELY sure?"
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
msgstr ""
-"Você ira remover o relacionamento de fork com o projeto original "
-"%{forked_from_project}. Tem certeza ABSOLUTA?"
-msgid ""
-"You are going to transfer %{project_name_with_namespace} to another owner. "
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Você irá transferir %{project_name_with_namespace} para outro proprietário. "
-"Tem certeza ABSOLUTA?"
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr "Você irá transferir %{project_name_with_namespace} para outro proprietário. Tem certeza ABSOLUTA?"
msgid "You can only add files when you are on a branch"
msgstr "Você somente pode adicionar arquivos quando estiver em um branch"
@@ -1214,31 +1420,20 @@ msgstr "Você não será notificado por email"
msgid "You will only receive notifications for the events you choose"
msgstr "Você será notificado apenas sobre eventos selecionados"
-msgid ""
-"You will only receive notifications for threads you have participated in"
+msgid "You will only receive notifications for threads you have participated in"
msgstr "Você será notificado apenas sobre tópicos nos quais participou"
msgid "You will receive notifications for any activity"
msgstr "Você será notificado sobre qualquer atividade"
-msgid ""
-"You will receive notifications only for comments in which you were "
-"@mentioned"
+msgid "You will receive notifications only for comments in which you were @mentioned"
msgstr "Você será notificado apenas sobre comentários que te @mencionam"
-msgid ""
-"You won't be able to pull or push project code via %{protocol} until you "
-"%{set_password_link} on your account"
-msgstr ""
-"Você não poderá fazer pull ou push via %{protocol} até que "
-"%{set_password_link} para sua conta"
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr "Você não poderá fazer pull ou push via %{protocol} até que %{set_password_link} para sua conta"
-msgid ""
-"You won't be able to pull or push project code via SSH until you "
-"%{add_ssh_key_link} to your profile"
-msgstr ""
-"Você não conseguirá fazer pull ou push no projeto via SSH até que adicione "
-"%{add_ssh_key_link} ao seu perfil"
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr "Você não conseguirá fazer pull ou push no projeto via SSH até que adicione %{add_ssh_key_link} ao seu perfil"
msgid "Your name"
msgstr "Seu nome"
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index 78f7b059077..96e6c8a8d3f 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -1,33 +1,20 @@
-# SAS <Stepanov.sa@bashkortostan.ru>, 2017. #zanata
-# Huang Tao <htve@outlook.com>, 2017. #zanata
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-13 12:07-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Russian\n"
+"Language: ru_RU\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Language-Team: Russian (https://translate.zanata.org/project/view/GitLab)\n"
-"PO-Revision-Date: 2017-08-06 11:23-0400\n"
-"Last-Translator: Ðндрей П. <fenixnow33@gmail.com>\n"
-"Language: ru\n"
-"X-Generator: Zanata 3.9.6\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
-"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
-
-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 добавленный коммит был иÑключен Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ "
-"производительноÑтью."
-msgstr[1] ""
-"%s добавленные коммиты были иÑключены Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ "
-"производительноÑтью."
-msgstr[2] ""
-"%s добавленные коммиты были иÑключены Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ "
-"производительноÑтью."
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: ru\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
msgid "%d commit"
msgid_plural "%d commits"
@@ -35,9 +22,33 @@ msgstr[0] "%d коммит"
msgstr[1] "%d коммитов"
msgstr[2] "%d коммитов"
+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 добавленный коммит был иÑключен Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñтью."
+msgstr[1] "%s добавленные коммиты были иÑключены Ð´Ð»Ñ Ð¿Ñ€ÐµÐ´Ð¾Ñ‚Ð²Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñтью."
+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."
+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] ""
+msgstr[2] ""
+
+msgid "(checkout the %{link} for information on how to install it)."
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 конвейер"
@@ -50,6 +61,18 @@ msgstr "Графики отноÑительно непрерывной интеÐ
msgid "About auto deploy"
msgstr "ÐвтоматичеÑкое развертывание"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr "Ðктивный"
@@ -66,36 +89,106 @@ msgid "Add License"
msgstr "Добавить лицензию"
msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr ""
-"Добавьте ключ SSH в Ñвой профиль, чтобы отправлÑÑ‚ÑŒ или получать код через "
-"SSH."
+msgstr "Добавьте ключ SSH в Ñвой профиль, чтобы отправлÑÑ‚ÑŒ или получать код через SSH."
msgid "Add new directory"
msgstr "Добавить каталог"
+msgid "All"
+msgstr ""
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
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 "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Приложить файл через drag &amp; drop или %{upload_link}"
+msgid "Authentication log"
+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 "Branch"
msgid_plural "Branches"
msgstr[0] "Ветка"
msgstr[1] "Ветки"
msgstr[2] "Ветки"
-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 ""
-"Ветка <strong>%{branch_name}</strong> Ñоздана. Ð”Ð»Ñ Ð½Ð°Ñтройки автоматичеÑкого "
-"Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð²Ñ‹Ð±ÐµÑ€ÐµÑ‚Ðµ GitLab CI Yaml-шаблон и зафикÑируйте изменениÑ. "
-"%{link_to_autodeploy_doc}"
+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 "Ветка <strong>%{branch_name}</strong> Ñоздана. Ð”Ð»Ñ Ð½Ð°Ñтройки автоматичеÑкого Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð²Ñ‹Ð±ÐµÑ€ÐµÑ‚Ðµ GitLab CI Yaml-шаблон и зафикÑируйте изменениÑ. %{link_to_autodeploy_doc}"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ПоиÑк веток"
@@ -121,12 +214,18 @@ msgstr "ПроÑмотр файлов"
msgid "ByAuthor|by"
msgstr "по автору"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "ÐаÑтройка CI"
msgid "Cancel"
msgstr "Отмена"
+msgid "Cancel edit"
+msgstr "Отменить редактирование"
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Выбрать в ветке"
@@ -145,6 +244,9 @@ msgstr "Журнал изменений"
msgid "Charts"
msgstr "Диаграммы"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Подобрать в Ñтом коммите"
@@ -205,6 +307,9 @@ msgstr "пропущено"
msgid "CiStatus|running"
msgstr "выполнÑетÑÑ"
+msgid "Comments"
+msgstr "Комментарии"
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "Коммит"
@@ -238,12 +343,18 @@ msgstr "ФикÑировано"
msgid "Compare"
msgstr "Сравнить"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "РуководÑтво учаÑтника"
msgid "Contributors"
msgstr "УчаÑтники"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "Копировать URL в буфер обмена"
@@ -253,12 +364,11 @@ msgstr "Копировать SHA коммита в буфер обмена"
msgid "Create New Directory"
msgstr "Создать директорию"
-msgid ""
-"Create a personal access token on your account to pull or push via "
-"%{protocol}."
-msgstr ""
-"Создать личный токен на аккаунте Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ отправки через "
-"%{protocol}."
+msgid "Create a new branch"
+msgstr "Создать новую ветку"
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr "Создать личный токен на аккаунте Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ отправки через %{protocol}."
msgid "Create directory"
msgstr "Создать директорию"
@@ -290,25 +400,14 @@ msgstr "СинтакÑÐ¸Ñ Cron"
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 ""
-"ÐаÑтраиваемые уровни уведомлений аналогичны уровню уведомлений в "
-"ÑоответÑтвии Ñ ÑƒÑ‡Ð°Ñтием. С наÑтраиваемыми уровнÑми уведомлений вы также "
-"будете получать ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¾ выбранных ÑобытиÑÑ…. Чтобы узнать больше, "
-"поÑмотрите %{notification_link}."
+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 "ÐаÑтраиваемые уровни уведомлений аналогичны уровню уведомлений в ÑоответÑтвии Ñ ÑƒÑ‡Ð°Ñтием. С наÑтраиваемыми уровнÑми уведомлений вы также будете получать ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¾ выбранных ÑобытиÑÑ…. Чтобы узнать больше, поÑмотрите %{notification_link}."
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 "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 "ÐапиÑание кода"
@@ -343,12 +442,21 @@ msgstr[0] "РазмеÑтить"
msgstr[1] "Размещение"
msgstr[2] "Размещение"
+msgid "Deploy Keys"
+msgstr ""
+
msgid "Description"
msgstr "ОпиÑание"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "Каталог"
+msgid "Discard changes"
+msgstr "Отменить изменениÑ"
+
msgid "Don't show again"
msgstr "Ðе показывать Ñнова"
@@ -385,6 +493,27 @@ msgstr "Редактировать"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Изменить раÑпиÑание конвейера %{id}"
+msgid "Emails"
+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 "Ежедневно (в 4:00)"
@@ -433,35 +562,71 @@ 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 "Ð¡ÐµÐºÑ†Ð¸Ñ Gitlab Runner"
+
msgid "Go to your fork"
msgstr "Перейти к вашему форку"
msgid "GoToYourFork|Fork"
msgstr "Форк"
+msgid "Group overview"
+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 "Home"
msgstr "ГлавнаÑ"
+msgid "Hooks"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "ОчиÑтка уÑпешно запущена"
msgid "Import repository"
msgstr "Импорт репозиториÑ"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr "УÑтановите Gitlab Runner ÑовмеÑтимый Ñ Gitlab CI"
+
msgid "Interval Pattern"
msgstr "Шаблон интервала"
msgid "Introducing Cycle Analytics"
msgstr "Внедрение Цикла Ðналитик"
-msgid "Jobs for last month"
-msgstr "Работы за прошлый меÑÑц"
-
-msgid "Jobs for last week"
-msgstr "Работы за прошлую неделю"
+msgid "Issue events"
+msgstr ""
-msgid "Jobs for last year"
-msgstr "Работы за прошлый год"
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "Отключено"
@@ -469,6 +634,9 @@ msgstr "Отключено"
msgid "LFSStatus|Enabled"
msgstr "Включено"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "ПоÑледний %d день"
@@ -484,6 +652,12 @@ msgstr "ПоÑледнее обновление"
msgid "Last commit"
msgstr "ПоÑледний коммит"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "Узнайте больше в"
@@ -496,18 +670,42 @@ 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] "Ограничение %d ÑобытиÑ"
msgstr[1] "Ограничение %d Ñобытий"
msgstr[2] "Ограничение %d Ñобытий"
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "Среднее"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "добавить ключ SSH"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Обращение"
@@ -607,6 +805,9 @@ msgstr "УчаÑтие"
msgid "NotificationLevel|Watch"
msgstr "ОтÑлеживать"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Фильтр"
@@ -616,9 +817,15 @@ msgstr "Открыто"
msgid "Options"
msgstr "ÐаÑтройки"
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
msgstr "Владелец"
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
msgstr "Конвейер"
@@ -631,6 +838,9 @@ msgstr "РаÑпиÑание конвейера"
msgid "Pipeline Schedules"
msgstr "РаÑпиÑÐ°Ð½Ð¸Ñ ÐºÐ¾Ð½Ð²ÐµÐ¹ÐµÑ€Ð¾Ð²"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Ðеудача:"
@@ -694,6 +904,15 @@ 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 "вÑе"
@@ -706,6 +925,15 @@ msgstr "Ñо Ñтадией"
msgid "Pipeline|with stages"
msgstr "Ñо ÑтадиÑми"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Проект '%{project_name}' добавлен в очередь на удаление."
@@ -721,27 +949,30 @@ msgstr "Проект '%{project_name}' удален."
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 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 ""
-"Ðачат ÑкÑпорт проекта. СÑылка Ð´Ð»Ñ ÑÐºÐ°Ñ‡Ð¸Ð²Ð°Ð½Ð¸Ñ Ð±ÑƒÐ´ÐµÑ‚ отправлена по Ñлектронной "
-"почте."
+msgstr "Ðачат ÑкÑпорт проекта. СÑылка Ð´Ð»Ñ ÑÐºÐ°Ñ‡Ð¸Ð²Ð°Ð½Ð¸Ñ Ð±ÑƒÐ´ÐµÑ‚ отправлена по Ñлектронной почте."
msgid "Project home"
msgstr "ДомашнÑÑ Ñтраница"
+msgid "Project overview"
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "Отключено"
@@ -763,11 +994,17 @@ msgstr "Этап"
msgid "ProjectNetworkGraph|Graph"
msgstr "Граф"
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Подробнее"
msgid "Readme"
-msgstr "Readme"
+msgstr ""
msgid "RefSwitcher|Branches"
msgstr "Ветки"
@@ -799,15 +1036,30 @@ 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 "Сохранить раÑпиÑание конвейра"
@@ -826,13 +1078,17 @@ msgstr "Выбрать формат архива"
msgid "Select a timezone"
msgstr "Выбор временной зоны"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "Выбор целевой ветки"
-msgid "Set a password on your account to pull or push via %{protocol}."
+msgid "Service Templates"
msgstr ""
-"УÑтановите пароль в Ñвоем аккаунте, чтобы отправлÑÑ‚ÑŒ или получать код через "
-"%{protocol}."
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr "УÑтановите пароль в Ñвоем аккаунте, чтобы отправлÑÑ‚ÑŒ или получать код через %{protocol}."
msgid "Set up CI"
msgstr "ÐаÑтройка CI"
@@ -846,21 +1102,36 @@ msgstr "ÐаÑтройка автоматичеÑкого развертыван
msgid "SetPasswordToCloneLink|set a password"
msgstr "уÑтановить пароль"
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Показано %d Ñобытие"
msgstr[1] "Показано %d Ñобытий"
msgstr[2] "Показано %d Ñобытий"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "ИÑходный код"
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
msgid "StarProject|Star"
msgstr "Отметить"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Ðачать %{new_merge_request} Ñ Ñтих изменений"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Переключить ветка/тег"
@@ -876,14 +1147,11 @@ msgstr "Теги"
msgid "Target Branch"
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."
+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 "ÐšÐ¾Ð»Ð»ÐµÐºÑ†Ð¸Ñ Ñобытий добавленных в данные Ñобранные Ð´Ð»Ñ Ñтого Ñтапа."
@@ -891,44 +1159,20 @@ 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 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 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 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 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 "ДоÑтуп к проекту возможен любым зарегиÑтрированным пользователем."
@@ -939,50 +1183,26 @@ 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 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 ""
-"Этап поÑтановки показывает Ð²Ñ€ÐµÐ¼Ñ Ð¼ÐµÐ¶Ð´Ñƒ ÑлиÑнием \"MR\" и развертыванием кода "
-"в производÑтвенной Ñреде. Данные будут автоматичеÑки добавлены поÑле "
-"Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² производÑтве первый раз."
+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 "Этап поÑтановки показывает Ð²Ñ€ÐµÐ¼Ñ Ð¼ÐµÐ¶Ð´Ñƒ ÑлиÑнием \"MR\" и развертыванием кода в производÑтвенной Ñреде. Данные будут автоматичеÑки добавлены поÑле Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² производÑтве первый раз."
-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 ""
-"Этап теÑÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¿Ð¾ÐºÐ°Ð·Ñ‹Ð²Ð°ÐµÑ‚ времÑ, которое GitLab CI занимает Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑка "
-"каждого конвейера Ð´Ð»Ñ ÑоответÑтвующего запроÑа на ÑлиÑние. Данные будут "
-"автоматичеÑки добавлены поÑле Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ñ‹ вашего первого конвейера."
+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 "Этап теÑÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¿Ð¾ÐºÐ°Ð·Ñ‹Ð²Ð°ÐµÑ‚ времÑ, которое GitLab CI занимает Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑка каждого конвейера Ð´Ð»Ñ ÑоответÑтвующего запроÑа на ÑлиÑние. Данные будут автоматичеÑки добавлены поÑле Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ñ‹ вашего первого конвейера."
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 ""
-"Среднее значение в Ñ€Ñду. Пример: между 3, 5, 9, Ñреднее 5, между 3, 5, 7, 8, "
-"Ñреднее (5+7)/2 = 6."
+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 "Среднее значение в Ñ€Ñду. Пример: между 3, 5, 9, Ñреднее 5, между 3, 5, 7, 8, Ñреднее (5+7)/2 = 6."
-msgid ""
-"This means you can not push code until you create an empty repository or "
-"import existing one."
+msgid "There are problems accessing Git storage: "
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 " Ð’Ñ€ÐµÐ¼Ñ Ð´Ð¾ начала Ð¿Ð¾Ð¿Ð°Ð´Ð°Ð½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ñ‹ в планировщик"
@@ -1152,6 +1372,9 @@ msgstr "Загрузить файл"
msgid "UploadLink|click to upload"
msgstr "кликните Ð´Ð»Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "ИÑпользуютÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ñ‹Ð¹ наÑтройки уведомлений"
@@ -1176,40 +1399,23 @@ 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}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Ð’Ñ‹ ÑобираетеÑÑŒ удалить %{group_name}.\n"
-"Удаленные группы ÐЕ МОГУТ быть воÑÑтановлены!\n"
-"Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить %{group_name}. Удаленные группы ÐЕ МОГУТ быть воÑÑтановлены! Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Вы хотите удалить %{project_name_with_namespace}.\n"
-"Удаленный проект ÐЕ МОЖЕТ быть воÑÑтановлен!\n"
-"Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Ð’Ñ‹ хотите удалить %{project_name_with_namespace}. Удаленный проект ÐЕ МОЖЕТ быть воÑÑтановлен! Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
-msgid ""
-"You are going to remove the fork relationship to source project "
-"%{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr ""
-"Ð’Ñ‹ ÑобираетеÑÑŒ удалить ÑвÑзь форка Ñ Ð¸Ñходным проектом "
-"%{forked_from_project}. Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить ÑвÑзь форка Ñ Ð¸Ñходным проектом %{forked_from_project}. Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
-msgid ""
-"You are going to transfer %{project_name_with_namespace} to another owner. "
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Ð’Ñ‹ ÑобираетеÑÑŒ передать проект %{project_name_with_namespace} другому "
-"владельцу. Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ передать проект %{project_name_with_namespace} другому владельцу. Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
msgid "You can only add files when you are on a branch"
msgstr "Ð’Ñ‹ можете добавлÑÑ‚ÑŒ только файлы, когда находитеÑÑŒ в ветке"
@@ -1229,34 +1435,20 @@ 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 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 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 ""
-"Ð’Ñ‹ не Ñможете получать и отправлÑÑ‚ÑŒ код проекта через %{protocol} пока "
-"%{set_password_link} в ваш аккаунт"
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr "Ð’Ñ‹ не Ñможете получать и отправлÑÑ‚ÑŒ код проекта через %{protocol} пока %{set_password_link} в ваш аккаунт"
-msgid ""
-"You won't be able to pull or push project code via SSH until you "
-"%{add_ssh_key_link} to your profile"
-msgstr ""
-"Ð’Ñ‹ не Ñможете получать и отправлÑÑ‚ÑŒ код проекта через SSH пока "
-"%{add_ssh_key_link} в ваш профиль."
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr "Ð’Ñ‹ не Ñможете получать и отправлÑÑ‚ÑŒ код проекта через SSH пока %{add_ssh_key_link} в ваш профиль."
msgid "Your name"
msgstr "Ваше имÑ"
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 78144d3755d..4d24140f3dc 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -1,30 +1,20 @@
-# Huang Tao <htve@outlook.com>, 2017. #zanata
-# Ðндрей Витюк <andruwa13@gmail.com>, 2017. #zanata
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-13 12:07-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Ukrainian\n"
+"Language: uk_UA\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Language-Team: Ukrainian (https://translate.zanata.org/project/view/GitLab)\n"
-"PO-Revision-Date: 2017-08-06 11:23-0400\n"
-"Last-Translator: Ðндрей Витюк <andruwa13@gmail.com>\n"
-"Language: uk\n"
-"X-Generator: Zanata 3.9.6\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
-"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
-
-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 доданий Комміт був виключений Ð´Ð»Ñ Ð·Ð°Ð¿Ð¾Ð±Ñ–Ð³Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ з продуктивніÑÑ‚ÑŽ."
-msgstr[1] ""
-"%s доданих коммітів були виключені Ð´Ð»Ñ Ð·Ð°Ð¿Ð¾Ð±Ñ–Ð³Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ з продуктивніÑÑ‚ÑŽ."
-msgstr[2] ""
-"%s доданих коммітів були виключені Ð´Ð»Ñ Ð·Ð°Ð¿Ð¾Ð±Ñ–Ð³Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ з продуктивніÑÑ‚ÑŽ."
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: uk\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
msgid "%d commit"
msgid_plural "%d commits"
@@ -32,9 +22,33 @@ msgstr[0] "%d комміт"
msgstr[1] "%d комміта"
msgstr[2] "%d коммітів"
+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 доданий Комміт був виключений Ð´Ð»Ñ Ð·Ð°Ð¿Ð¾Ð±Ñ–Ð³Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ з продуктивніÑÑ‚ÑŽ."
+msgstr[1] "%s доданих коммітів були виключені Ð´Ð»Ñ Ð·Ð°Ð¿Ð¾Ð±Ñ–Ð³Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ з продуктивніÑÑ‚ÑŽ."
+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."
+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] ""
+msgstr[2] ""
+
+msgid "(checkout the %{link} for information on how to install it)."
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] "1 конвеєр"
@@ -47,6 +61,18 @@ msgstr "Це набір графічних елементів Ð´Ð»Ñ Ð±ÐµÐ·Ð¿ÐµÑ
msgid "About auto deploy"
msgstr "Про авто розгортаннÑ"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr "Ðктивний"
@@ -63,36 +89,106 @@ msgid "Add License"
msgstr "Додати ліцензію"
msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr ""
-"Додати SSH ключа в Ñвій профіль, щоб мати можливіÑÑ‚ÑŒ завантажити чи "
-"надіÑлати зміни через SSH."
+msgstr "Додати SSH ключа в Ñвій профіль, щоб мати можливіÑÑ‚ÑŒ завантажити чи надіÑлати зміни через SSH."
msgid "Add new directory"
msgstr "Додати новий каталог"
+msgid "All"
+msgstr "Ð’ÑÑ–"
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
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 "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Прикріпити файл за допомогою перетÑÐ³ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð±Ð¾ %{upload_link}"
+msgid "Authentication log"
+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 "Branch"
msgid_plural "Branches"
msgstr[0] "Гілка"
msgstr[1] "Гілки"
msgstr[2] "Гілок"
-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 ""
-"Гілка <strong>%{branch_name}</strong> Ñтворена. Ð”Ð»Ñ Ð½Ð°Ñтройки автоматичного "
-"Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð²Ð¸Ð±ÐµÑ€Ñ–Ñ‚ÑŒ GitLab CI Yaml-шаблон Ñ– закоммітьте зміни. "
-"%{link_to_autodeploy_doc}"
+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 "Гілка <strong>%{branch_name}</strong> Ñтворена. Ð”Ð»Ñ Ð½Ð°Ñтройки автоматичного Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð²Ð¸Ð±ÐµÑ€Ñ–Ñ‚ÑŒ GitLab CI Yaml-шаблон Ñ– закоммітьте зміни. %{link_to_autodeploy_doc}"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Пошук гілок"
@@ -118,12 +214,18 @@ msgstr "ПереглÑд файлів"
msgid "ByAuthor|by"
msgstr "від"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI"
msgid "Cancel"
msgstr "СкаÑувати"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Вибрати в гілці"
@@ -142,6 +244,9 @@ msgstr "СпиÑок змін (Changelog)"
msgid "Charts"
msgstr "Графіки"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "Cherry-pick в цьому комміті"
@@ -202,6 +307,9 @@ msgstr "пропущено"
msgid "CiStatus|running"
msgstr "виконуєтьÑÑ"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "Комміт"
@@ -235,12 +343,18 @@ msgstr "Комміт від"
msgid "Compare"
msgstr "ПорівнÑти"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "Керівництво контриб’юторів"
msgid "Contributors"
msgstr "Контриб’ютори"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "Скопіювати URL в буфер обміну"
@@ -250,12 +364,11 @@ msgstr "Скопіювати ідентифікатор в буфер обмін
msgid "Create New Directory"
msgstr "Створити новий каталог"
-msgid ""
-"Create a personal access token on your account to pull or push via "
-"%{protocol}."
+msgid "Create a new branch"
msgstr ""
-"Створити токен доÑтупу Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ аккауета, щоб відправлÑти або отримувати "
-"через %{protocol}."
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr "Створити токен доÑтупу Ð´Ð»Ñ Ð²Ð°ÑˆÐ¾Ð³Ð¾ аккауета, щоб відправлÑти або отримувати через %{protocol}."
msgid "Create directory"
msgstr "Створити каталог"
@@ -287,24 +400,14 @@ msgstr "СинтакÑÐ¸Ñ Cron"
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 ""
-"Спеціальні рівні Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñпівпадають з рівнем учаÑÑ‚Ñ–. За допомогою "
-"Ñпеціальних рівнів Ñповіщень ви також отримуватимете ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ вибрані "
-"події. Щоб дізнатиÑÑŒ більше, переглÑньте %{notification_link}."
+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 "Спеціальні рівні Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ñпівпадають з рівнем учаÑÑ‚Ñ–. За допомогою Ñпеціальних рівнів Ñповіщень ви також отримуватимете ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ вибрані події. Щоб дізнатиÑÑŒ більше, переглÑньте %{notification_link}."
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 "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 "Код"
@@ -339,12 +442,21 @@ msgstr[0] "РозгортаннÑ"
msgstr[1] "РозгортаннÑ"
msgstr[2] "Розгортань"
+msgid "Deploy Keys"
+msgstr ""
+
msgid "Description"
msgstr "ОпиÑ"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "Ім'Ñ ÐºÐ°Ñ‚Ð°Ð»Ð¾Ð³Ñƒ"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "Ðе показувати знову"
@@ -381,6 +493,27 @@ msgstr "Редагувати"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Редагувати Розклад Конвеєра %{id}"
+msgid "Emails"
+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 "Кожен день (в 4:00 ранку)"
@@ -429,35 +562,71 @@ 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 "Group overview"
+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 "Home"
msgstr "Головна"
+msgid "Hooks"
+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 "Jobs for last month"
-msgstr "КількіÑÑ‚ÑŒ завдань за оÑтанній міÑÑць"
-
-msgid "Jobs for last week"
-msgstr "КількіÑÑ‚ÑŒ завдань за оÑтанній тиждень"
+msgid "Issue events"
+msgstr ""
-msgid "Jobs for last year"
-msgstr "КількіÑÑ‚ÑŒ завдань за оÑтанній рік"
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "Вимкнено"
@@ -465,6 +634,9 @@ msgstr "Вимкнено"
msgid "LFSStatus|Enabled"
msgstr "Увімкнено"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "ОÑтанній %d день"
@@ -480,6 +652,12 @@ msgstr "ОÑтаннє оновленнÑ"
msgid "Last commit"
msgstr "ОÑтанній комміт"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "ДізнайтеÑÑŒ більше"
@@ -492,18 +670,42 @@ 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] "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ %d події"
msgstr[1] "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ %d подій"
msgstr[2] "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ %d подій"
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "Медіана"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "не додаÑте SSH ключ"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Ðова проблема"
@@ -603,6 +805,9 @@ msgstr "Берете учаÑÑ‚ÑŒ"
msgid "NotificationLevel|Watch"
msgstr "ВідÑтежувати"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "Фільтр"
@@ -612,9 +817,15 @@ msgstr "Відкрито"
msgid "Options"
msgstr "Параметри"
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
msgstr "ВлаÑник"
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
msgstr "Конвеєр"
@@ -627,6 +838,9 @@ msgstr "Розклад Конвеєра"
msgid "Pipeline Schedules"
msgstr "Розклади Конвеєрів"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Ðе вдалоÑÑ:"
@@ -690,6 +904,15 @@ 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 "вÑÑ–"
@@ -702,6 +925,15 @@ msgstr "зі Ñтадією"
msgid "Pipeline|with stages"
msgstr "зі ÑтадіÑми"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Проект '%{project_name}' доданий в чергу на видаленнÑ."
@@ -717,27 +949,30 @@ msgstr "Проект '%{project_name}' видалений."
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 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 ""
-"Розпочато екÑпорт проекту. ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ ÑÐºÐ°Ñ‡ÑƒÐ²Ð°Ð½Ð½Ñ Ð±ÑƒÐ´Ðµ надіÑлана "
-"електронною поштою."
+msgstr "Розпочато екÑпорт проекту. ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð´Ð»Ñ ÑÐºÐ°Ñ‡ÑƒÐ²Ð°Ð½Ð½Ñ Ð±ÑƒÐ´Ðµ надіÑлана електронною поштою."
msgid "Project home"
msgstr "Ð”Ð¾Ð¼Ð°ÑˆÐ½Ñ Ñторінка проекту"
+msgid "Project overview"
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "Вимкнено"
@@ -759,6 +994,12 @@ msgstr "Етап"
msgid "ProjectNetworkGraph|Graph"
msgstr "ІÑторіÑ"
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Докладніше"
@@ -795,15 +1036,30 @@ 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 "Зберегти Розклад Конвеєра"
@@ -822,13 +1078,17 @@ msgstr "Виберіть формат архіву"
msgid "Select a timezone"
msgstr "Вибрати чаÑовий поÑÑ"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "Вибір цільової гілки"
-msgid "Set a password on your account to pull or push via %{protocol}."
+msgid "Service Templates"
msgstr ""
-"Ð’Ñтановіть пароль Ñвого облікового запиÑу, щоб відправлÑти або отримувати "
-"код через %{protocol}."
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr "Ð’Ñтановіть пароль Ñвого облікового запиÑу, щоб відправлÑти або отримувати код через %{protocol}."
msgid "Set up CI"
msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI"
@@ -842,21 +1102,36 @@ msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡Ð½Ðµ розгортаннÑ"
msgid "SetPasswordToCloneLink|set a password"
msgstr "вÑтановити пароль"
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Показано %d подію"
msgstr[1] "Показано %d події"
msgstr[2] "Показано %d подій"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "Код"
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
msgid "StarProject|Star"
msgstr "ПідпиÑатиÑÑ"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Почати %{new_merge_request} з цих змін"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "тег"
@@ -872,14 +1147,11 @@ msgstr "Теги"
msgid "Target Branch"
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."
+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 "ÐšÐ¾Ð»ÐµÐºÑ†Ñ–Ñ Ð¿Ð¾Ð´Ñ–Ð¹ додана до даних, зібраних Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ етапу."
@@ -887,43 +1159,20 @@ 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 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 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 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 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 "ДоÑтуп до проекту можливий будь-Ñким зареєÑтрованим кориÑтувачем."
@@ -934,49 +1183,26 @@ 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 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 ""
-"Ð¡Ñ‚Ð°Ð´Ñ–Ñ Ð”Ð•Ð’ показує Ñ‡Ð°Ñ Ð¼Ñ–Ð¶ злиттÑм \"MR\" та розгортаннÑм коду у ПРОДакшин. "
-"Дані автоматично додаютьÑÑ Ð¿Ñ–ÑÐ»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ñƒ ПРОДакшин вперше."
+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 "Ð¡Ñ‚Ð°Ð´Ñ–Ñ Ð”Ð•Ð’ показує Ñ‡Ð°Ñ Ð¼Ñ–Ð¶ злиттÑм \"MR\" та розгортаннÑм коду у ПРОДакшин. Дані автоматично додаютьÑÑ Ð¿Ñ–ÑÐ»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ñƒ ПРОДакшин вперше."
-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 ""
-"Ð¡Ñ‚Ð°Ð´Ñ–Ñ Ñ‚ÐµÑÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾ÐºÐ°Ð·ÑƒÑ” чаÑ, Ñкий GitLab CI виконує Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑку кожного "
-"конвеєра Ð´Ð»Ñ Ð²Ñ–Ð´Ð¿Ð¾Ð²Ñ–Ð´Ð½Ð¾Ð³Ð¾ запиту злиттÑ. Дані будуть автоматично додані "
-"піÑÐ»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ конвеєра."
+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 "Ð¡Ñ‚Ð°Ð´Ñ–Ñ Ñ‚ÐµÑÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾ÐºÐ°Ð·ÑƒÑ” чаÑ, Ñкий GitLab CI виконує Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑку кожного конвеєра Ð´Ð»Ñ Ð²Ñ–Ð´Ð¿Ð¾Ð²Ñ–Ð´Ð½Ð¾Ð³Ð¾ запиту злиттÑ. Дані будуть автоматично додані піÑÐ»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ конвеєра."
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 ""
-"Середнє Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð² Ñ€Ñдку. Приклад: між 3, 5, 9, Ñередніми 5, між 3, 5, 7, 8, "
-"Ñередніми (5 + 7) / 2 = 6."
+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 "Середнє Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð² Ñ€Ñдку. Приклад: між 3, 5, 9, Ñередніми 5, між 3, 5, 7, 8, Ñередніми (5 + 7) / 2 = 6."
-msgid ""
-"This means you can not push code until you create an empty repository or "
-"import existing one."
+msgid "There are problems accessing Git storage: "
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 "Ð§Ð°Ñ Ð´Ð¾ початку потраплÑÐ½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð¸ в планувальник"
@@ -1146,6 +1372,9 @@ msgstr "Завантажити файл"
msgid "UploadLink|click to upload"
msgstr "ÐатиÑніть, щоб завантажити"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "ВикориÑтовуютьÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ñ– Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ"
@@ -1170,40 +1399,23 @@ 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}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Ви хочете видалити %{group_name}.\n"
-"Видалені групи ÐЕ МОЖÐРбуду відновити!\n"
-"Ви ÐБСОЛЮТÐО впевнені?"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Ви хочете видалити %{group_name}. Видалені групи ÐЕ МОЖÐРбуду відновити! Ви ÐБСОЛЮТÐО впевнені?"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Ви хочете видалити %{project_name_with_namespace}.\n"
-"Видалений проект ÐЕ МОЖЕ бути відновлений!\n"
-"Ви ÐБСОЛЮТÐО впевнені?"
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Ви хочете видалити %{project_name_with_namespace}. Видалений проект ÐЕ МОЖЕ бути відновлений! Ви ÐБСОЛЮТÐО впевнені?"
-msgid ""
-"You are going to remove the fork relationship to source project "
-"%{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr ""
-"Ви збираєтеÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ зв'Ñзок з форка з вихідним проектом "
-"%{forked_from_project}. Ви ÐБСОЛЮТÐО впевнені?"
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr "Ви збираєтеÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ зв'Ñзок з форка з вихідним проектом %{forked_from_project}. Ви ÐБСОЛЮТÐО впевнені?"
-msgid ""
-"You are going to transfer %{project_name_with_namespace} to another owner. "
-"Are you ABSOLUTELY sure?"
-msgstr ""
-"Ви збираєтеÑÑ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‚Ð¸ проект %{project_name_with_namespace} іншому влаÑнику."
-" Ви ÐБСОЛЮТÐО впевнені?"
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr "Ви збираєтеÑÑ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‚Ð¸ проект %{project_name_with_namespace} іншому влаÑнику. Ви ÐБСОЛЮТÐО впевнені?"
msgid "You can only add files when you are on a branch"
msgstr "Ви можете додавати тільки файли, коли перебуваєте в гілці"
@@ -1223,35 +1435,20 @@ 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 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 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 ""
-"Ви не зможете отримувати Ñ– відправлÑти код проекту через %{protocol} поки "
-"%{set_password_link} в ваш аккаунт"
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr "Ви не зможете отримувати Ñ– відправлÑти код проекту через %{protocol} поки %{set_password_link} в ваш аккаунт"
-msgid ""
-"You won't be able to pull or push project code via SSH until you "
-"%{add_ssh_key_link} to your profile"
-msgstr ""
-"Ви не зможете отримувати Ñ– відправлÑти код проекту через SSH поки "
-"%{add_ssh_key_link} в ваш профіль."
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr "Ви не зможете отримувати Ñ– відправлÑти код проекту через SSH поки %{add_ssh_key_link} в ваш профіль."
msgid "Your name"
msgstr "Ваше ім'Ñ"
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index 4a550db55d2..47de28209df 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -1,36 +1,51 @@
-# Huang Tao <htve@outlook.com>, 2017. #zanata
-# Xiaogang Wen <xiaogang@gitlab.com>, 2017.
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-05 08:50-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Chinese Simplified\n"
+"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-07-12 06:23-0400\n"
-"Last-Translator: Huang Tao <htve@outlook.com>\n"
-"Language-Team: Chinese (China) (https://translate.zanata.org/project/view/GitLab)\n"
-"Language: zh-CN\n"
-"X-Generator: Zanata 3.9.6\n"
-"Plural-Forms: nplurals=1; plural=0\n"
-
-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 次æ交。"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: zh-CN\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d 次æ交"
+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 次æ交。"
+
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."
+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 "已失败 %{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 "已失败 %{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] "%{storage_name}:已 %{failed_attempts} 次å°è¯•è®¿é—®å­˜å‚¨å¤±è´¥ï¼š"
+
+msgid "(checkout the %{link} for information on how to install it)."
+msgstr "(如需了解更多的安装信æ¯ï¼Œè¯·æŸ¥çœ‹ %{link})"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "1 æ¡æµæ°´çº¿"
-msgstr[1] "%d æ¡æµæ°´çº¿"
+msgstr[0] "%d æ¡æµæ°´çº¿"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "æŒç»­é›†æˆæ•°æ®å›¾"
@@ -38,6 +53,18 @@ msgstr "æŒç»­é›†æˆæ•°æ®å›¾"
msgid "About auto deploy"
msgstr "关于自动部署"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr "å¯ç”¨"
@@ -59,26 +86,99 @@ msgstr "新建一个用于推é€æˆ–拉å–çš„ SSH 秘钥到账å·ä¸­ã€‚"
msgid "Add new directory"
msgstr "添加目录"
+msgid "All"
+msgstr "全部"
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
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 "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放文件到此处或者 %{upload_link}"
+msgid "Authentication log"
+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 "Branch"
msgid_plural "Branches"
msgstr[0] "分支"
-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 ""
-"已创建分支 <strong>%{branch_name}</strong> 。如需设置自动部署, 请选择åˆé€‚çš„ GitLab CI Yaml "
-"模æ¿å¹¶æ交更改。%{link_to_autodeploy_doc}"
+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 "已创建分支 <strong>%{branch_name}</strong> 。如需设置自动部署, 请选择åˆé€‚çš„ GitLab CI Yaml 模æ¿å¹¶æ交更改。%{link_to_autodeploy_doc}"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "æœç´¢åˆ†æ”¯"
@@ -104,12 +204,18 @@ msgstr "æµè§ˆæ–‡ä»¶"
msgid "ByAuthor|by"
msgstr "作者:"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "CI é…ç½®"
msgid "Cancel"
msgstr "å–消"
+msgid "Cancel edit"
+msgstr "å–消编辑"
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "选择分支"
@@ -128,6 +234,9 @@ msgstr "更新日志"
msgid "Charts"
msgstr "统计图"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "优选此æ交"
@@ -188,6 +297,9 @@ msgstr "已跳过"
msgid "CiStatus|running"
msgstr "è¿è¡Œä¸­"
+msgid "Comments"
+msgstr "评论"
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "æ交"
@@ -219,12 +331,18 @@ msgstr "æ交者:"
msgid "Compare"
msgstr "比较"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "贡献指å—"
msgid "Contributors"
msgstr "贡献者"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "å¤åˆ¶ URL 到剪贴æ¿"
@@ -234,9 +352,10 @@ msgstr "å¤åˆ¶æ交 SHA 的值到剪贴æ¿"
msgid "Create New Directory"
msgstr "创建新目录"
-msgid ""
-"Create a personal access token on your account to pull or push via "
-"%{protocol}."
+msgid "Create a new branch"
+msgstr "创建一个新分支"
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "在å¸æˆ·ä¸Šåˆ›å»ºä¸ªäººè®¿é—®ä»¤ç‰Œï¼Œä»¥é€šè¿‡ %{protocol} æ¥æ‹‰å–或推é€ã€‚"
msgid "Create directory"
@@ -269,19 +388,13 @@ msgstr "Cron 语法"
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 ""
-"自定义通知级别继承自å‚与级别。使用自定义通知级别,您会收到å‚与级别åŠé€‰å®šäº‹ä»¶çš„通知。想了解更多信æ¯ï¼Œè¯·æŸ¥çœ‹ %{notification_link}."
+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 "自定义通知级别继承自å‚与级别。使用自定义通知级别,您会收到å‚与级别åŠé€‰å®šäº‹ä»¶çš„通知。想了解更多信æ¯ï¼Œè¯·æŸ¥çœ‹ %{notification_link}."
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."
+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"
@@ -315,12 +428,21 @@ msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "部署"
+msgid "Deploy Keys"
+msgstr ""
+
msgid "Description"
msgstr "æè¿°"
+msgid "Details"
+msgstr "详情"
+
msgid "Directory name"
msgstr "目录å称"
+msgid "Discard changes"
+msgstr "放弃更改"
+
msgid "Don't show again"
msgstr "ä¸å†æ˜¾ç¤º"
@@ -357,6 +479,27 @@ msgstr "编辑"
msgid "Edit Pipeline Schedule %{id}"
msgstr "编辑 %{id} æµæ°´çº¿è®¡åˆ’"
+msgid "Emails"
+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 "æ¯æ—¥æ‰§è¡Œï¼ˆå‡Œæ™¨ 4 点)"
@@ -403,35 +546,71 @@ 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 "Git 存储å¥åº·ä¿¡æ¯å·²é‡ç½®"
+
+msgid "GitLab Runner section"
+msgstr "GitLab Runner"
+
msgid "Go to your fork"
msgstr "跳转到派生项目"
msgid "GoToYourFork|Fork"
msgstr "跳转到派生项目"
+msgid "Group overview"
+msgstr ""
+
+msgid "Health Check"
+msgstr "å¥åº·æ£€æŸ¥"
+
+msgid "Health information can be retrieved from the following endpoints. More information is available"
+msgstr "å¥åº·ä¿¡æ¯å¯ä»¥ä»Žä»¥ä¸‹API路径获å–。如需了解更多信æ¯ï¼Œè¯·æŸ¥çœ‹"
+
+msgid "HealthCheck|Access token is"
+msgstr "访问令牌是"
+
+msgid "HealthCheck|Healthy"
+msgstr "å¥åº·"
+
+msgid "HealthCheck|No Health Problems Detected"
+msgstr "没有检测到å¥åº·é—®é¢˜"
+
+msgid "HealthCheck|Unhealthy"
+msgstr "éžå¥åº·"
+
msgid "Home"
msgstr "首页"
+msgid "Hooks"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "已开始维护"
msgid "Import repository"
msgstr "导入存储库"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr "安装一个与 GitLab CI 兼容的 Runner"
+
msgid "Interval Pattern"
msgstr "循环周期"
msgid "Introducing Cycle Analytics"
msgstr "周期分æžç®€ä»‹"
-msgid "Jobs for last month"
-msgstr "上个月的作业"
-
-msgid "Jobs for last week"
-msgstr "上个星期的作业"
+msgid "Issue events"
+msgstr "议题事件"
-msgid "Jobs for last year"
-msgstr "去年的作业"
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "åœç”¨"
@@ -439,6 +618,9 @@ msgstr "åœç”¨"
msgid "LFSStatus|Enabled"
msgstr "å¯ç”¨"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "最近 %d 天"
@@ -452,6 +634,12 @@ msgstr "最åŽæ›´æ–°"
msgid "Last commit"
msgstr "最åŽæ交"
+msgid "LastPushEvent|You pushed to"
+msgstr "您推é€äº†"
+
+msgid "LastPushEvent|at"
+msgstr "于"
+
msgid "Learn more in the"
msgstr "了解更多"
@@ -464,16 +652,40 @@ 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] "最多显示 %d 个事件"
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "中ä½æ•°"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr "åˆå¹¶äº‹ä»¶"
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "新建 SSH 公钥"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr "帮助文档"
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新建议题"
@@ -571,6 +783,9 @@ msgstr "å‚与"
msgid "NotificationLevel|Watch"
msgstr "关注"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "筛选"
@@ -580,9 +795,15 @@ msgstr "开始于"
msgid "Options"
msgstr "æ“作"
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
msgstr "所有者"
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
msgstr "æµæ°´çº¿"
@@ -595,6 +816,9 @@ msgstr "æµæ°´çº¿è®¡åˆ’"
msgid "Pipeline Schedules"
msgstr "æµæ°´çº¿è®¡åˆ’"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "失败:"
@@ -658,6 +882,15 @@ 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 "所有"
@@ -670,6 +903,15 @@ msgstr "于阶段"
msgid "Pipeline|with stages"
msgstr "于阶段"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr "项目"
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "项目 '%{project_name}' 已进入删除队列。"
@@ -685,15 +927,16 @@ msgstr "项目 '%{project_name}' 将被删除。"
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."
+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."
@@ -702,6 +945,12 @@ msgstr "项目导出已开始。下载链接将通过电å­é‚®ä»¶å‘é€ã€‚"
msgid "Project home"
msgstr "项目首页"
+msgid "Project overview"
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr "订阅"
+
msgid "ProjectFeature|Disabled"
msgstr "åœç”¨"
@@ -723,6 +972,12 @@ msgstr "阶段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支图"
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr "推é€äº‹ä»¶"
+
msgid "Read more"
msgstr "了解更多"
@@ -759,15 +1014,30 @@ msgstr "ç¨åŽæ醒"
msgid "Remove project"
msgstr "删除项目"
+msgid "Repository"
+msgstr "存储库"
+
msgid "Request Access"
msgstr "申请æƒé™"
+msgid "Reset git storage health information"
+msgstr "é‡ç½® Git 存储的å¥åº·ä¿¡æ¯"
+
+msgid "Reset health check access token"
+msgstr "é‡ç½®å¥åº·æ£€æŸ¥è®¿é—®ä»¤ç‰Œ"
+
+msgid "Reset runners registration token"
+msgstr "é‡ç½® Runner 注册令牌"
+
msgid "Revert this commit"
msgstr "还原此æ交"
msgid "Revert this merge request"
msgstr "还原此åˆå¹¶è¯·æ±‚"
+msgid "SSH Keys"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "ä¿å­˜æµæ°´çº¿è®¡åˆ’"
@@ -786,9 +1056,15 @@ 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 "为账å·åˆ›å»ºä¸€ä¸ªç”¨äºŽæŽ¨é€æˆ–拉å–çš„ %{protocol} 密ç ã€‚"
@@ -804,19 +1080,34 @@ msgstr "设置自动部署"
msgid "SetPasswordToCloneLink|set a password"
msgstr "设置密ç "
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "显示 %d 个事件"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "æºä»£ç "
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr "在 Runner 设置时指定以下 URL:"
+
msgid "StarProject|Star"
msgstr "星标"
msgid "Start a %{new_merge_request} with these changes"
msgstr "由此更改 %{new_merge_request}"
+msgid "Start the Runner!"
+msgstr "å¯åŠ¨ Runner!"
+
msgid "Switch branch/tag"
msgstr "切æ¢åˆ†æ”¯/标签"
@@ -830,10 +1121,10 @@ msgstr "标签"
msgid "Target Branch"
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."
+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."
@@ -842,31 +1133,19 @@ 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."
+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."
+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."
+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."
+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."
@@ -878,36 +1157,25 @@ 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."
+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."
+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."
+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 "测试阶段概述了 GitLab CI 为相关åˆå¹¶è¯·æ±‚è¿è¡Œæ¯ä¸ªæµæ°´çº¿æ‰€éœ€çš„时间。当第一个æµæ°´çº¿è¿è¡Œå®ŒæˆåŽï¼Œæ•°æ®å°†è‡ªåŠ¨æ·»åŠ åˆ°æ­¤å¤„。"
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."
+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 "中ä½æ•°æ˜¯ä¸€ä¸ªæ•°åˆ—中最中间的值。例如在 3ã€5ã€9 之间,中ä½æ•°æ˜¯ 5。在 3ã€5ã€7ã€8 之间,中ä½æ•°æ˜¯ (5 + 7)/ 2 = 6。"
-msgid ""
-"This means you can not push code until you create an empty repository or "
-"import existing one."
+msgid "There are problems accessing Git storage: "
+msgstr "访问 Git 存储时出现问题:"
+
+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"
@@ -1074,6 +1342,9 @@ msgstr "上传文件"
msgid "UploadLink|click to upload"
msgstr "点击上传"
+msgid "Use the following registration token during setup:"
+msgstr "在安装过程中使用以下注册令牌:"
+
msgid "Use your global notification setting"
msgstr "使用全局通知设置"
@@ -1098,31 +1369,22 @@ 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}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr "å³å°†åˆ é™¤ %{group_name}。\n"
-"已删除的群组无法æ¢å¤ï¼\n"
-"确定继续å—?"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "å³å°†åˆ é™¤ %{group_name}。已删除的群组无法æ¢å¤ï¼ç¡®å®šç»§ç»­å—?"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr "å³å°†è¦åˆ é™¤ %{project_name_with_namespace}。已删除的项目无法æ¢å¤ï¼ç¡®å®šç»§ç»­å—?"
-msgid ""
-"You are going to remove the fork relationship to source project "
-"%{forked_from_project}. Are you ABSOLUTELY sure?"
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
msgstr "å³å°†åˆ é™¤ä¸Žæºé¡¹ç›® %{forked_from_project} 的派生关系。确定继续å—?"
-msgid ""
-"You are going to transfer %{project_name_with_namespace} to another owner. "
-"Are you ABSOLUTELY sure?"
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "å³å°† %{project_name_with_namespace} 转移给å¦ä¸€ä¸ªæ‰€æœ‰è€…。确定继续å—?"
msgid "You can only add files when you are on a branch"
@@ -1143,26 +1405,19 @@ 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"
+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"
+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"
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
msgstr "在账å·ä¸­ %{set_password_link} 之å‰å°†æ— æ³•é€šè¿‡ %{protocol} 拉å–或推é€ä»£ç ã€‚"
-msgid ""
-"You won't be able to pull or push project code via SSH until you "
-"%{add_ssh_key_link} to your profile"
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "在账å·ä¸­ %{add_ssh_key_link} 之å‰å°†æ— æ³•é€šè¿‡ SSH 拉å–或推é€ä»£ç ã€‚"
msgid "Your name"
@@ -1181,3 +1436,4 @@ msgstr "通知邮件"
msgid "parent"
msgid_plural "parents"
msgstr[0] "父级"
+
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index 69b2bf80dbf..fee0d661c7a 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -1,35 +1,51 @@
-# Huang Tao <htve@outlook.com>, 2017. #zanata
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-05 08:50-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:21-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Chinese Traditional, Hong Kong\n"
+"Language: zh_HK\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-07-12 06:32-0400\n"
-"Last-Translator: Huang Tao <htve@outlook.com>\n"
-"Language-Team: Chinese (Hong Kong SAR China) (https://translate.zanata.org/project/view/GitLab)\n"
-"Language: zh-HK\n"
-"X-Generator: Zanata 3.9.6\n"
-"Plural-Forms: nplurals=1; plural=0\n"
-
-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 次æ交。"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: zh-HK\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] " %d 次æ交"
+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 次æ交。"
+
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."
+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 "已失敗 %{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 "已失敗 %{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] "%{storage_name}:已訪å•æ­¤ä¸»æ©Ÿå¤±æ•— %{failed_attempts} 次"
+
+msgid "(checkout the %{link} for information on how to install it)."
+msgstr "(想了解更多的安è£è¨Šæ¯è«‹æŸ¥çœ‹ %{link})"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "1 æ¢æµæ°´ç·š"
-msgstr[1] "%d æ¢æµæ°´ç·š"
+msgstr[0] "%d æ¢æµæ°´ç·š"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "相關æŒçºŒé›†æˆçš„圖åƒé›†åˆ"
@@ -37,6 +53,18 @@ msgstr "相關æŒçºŒé›†æˆçš„圖åƒé›†åˆ"
msgid "About auto deploy"
msgstr "關於自動部署"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 ""
+
msgid "Active"
msgstr "啟用"
@@ -58,26 +86,99 @@ msgstr "新增壹個用於推é€æˆ–拉å–çš„ SSH 秘鑰到賬號中。"
msgid "Add new directory"
msgstr "添加新目錄"
+msgid "All"
+msgstr "全部"
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
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 "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放文件到此處或者 %{upload_link}"
+msgid "Authentication log"
+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 "Branch"
msgid_plural "Branches"
msgstr[0] "分支"
-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 ""
-"分支 <strong>%{branch_name}</strong> 已創建。如需設置自動部署, è«‹é¸æ“‡åˆé©çš„ GitLab CI Yaml "
-"模æ¿ä½µæ交更改。%{link_to_autodeploy_doc}"
+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 "分支 <strong>%{branch_name}</strong> 已創建。如需設置自動部署, è«‹é¸æ“‡åˆé©çš„ GitLab CI Yaml 模æ¿ä½µæ交更改。%{link_to_autodeploy_doc}"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "æœç´¢åˆ†æ”¯"
@@ -103,12 +204,18 @@ msgstr "ç€è¦½æ–‡ä»¶"
msgid "ByAuthor|by"
msgstr "作者:"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "CI é…ç½®"
msgid "Cancel"
msgstr "å–消"
+msgid "Cancel edit"
+msgstr "å–消编辑"
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "挑é¸åˆ°åˆ†æ”¯"
@@ -127,6 +234,9 @@ msgstr "更新日誌"
msgid "Charts"
msgstr "統計圖"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "優é¸æ­¤æ交"
@@ -187,6 +297,9 @@ msgstr "已跳éŽ"
msgid "CiStatus|running"
msgstr "é‹è¡Œä¸­"
+msgid "Comments"
+msgstr "è©•è«– (Comment)"
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "æ交"
@@ -218,12 +331,18 @@ msgstr "æ交者:"
msgid "Compare"
msgstr "比較"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "è²¢ç»æŒ‡å—"
msgid "Contributors"
msgstr "è²¢ç»è€…"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "複製URL到剪貼æ¿"
@@ -233,9 +352,10 @@ msgstr "複製æ交 SHA 到剪貼æ¿"
msgid "Create New Directory"
msgstr "創建新目錄"
-msgid ""
-"Create a personal access token on your account to pull or push via "
-"%{protocol}."
+msgid "Create a new branch"
+msgstr "創建壹個新分支 (branch)"
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "在帳戶上創建個人訪å•ä»¤ç‰Œï¼Œä»¥é€šéŽ %{protocol} 來拉å–或推é€ã€‚"
msgid "Create directory"
@@ -268,19 +388,13 @@ msgstr "Cron 語法"
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 ""
-"自定義通知級別繼承自åƒèˆ‡ç´šåˆ¥ã€‚使用自定義通知級別,您會收到åƒèˆ‡ç´šåˆ¥åŠé¸å®šäº‹ä»¶çš„通知。想了解更多信æ¯ï¼Œè«‹æŸ¥çœ‹ %{notification_link}."
+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 "自定義通知級別繼承自åƒèˆ‡ç´šåˆ¥ã€‚使用自定義通知級別,您會收到åƒèˆ‡ç´šåˆ¥åŠé¸å®šäº‹ä»¶çš„通知。想了解更多信æ¯ï¼Œè«‹æŸ¥çœ‹ %{notification_link}."
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."
+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"
@@ -314,12 +428,21 @@ msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "部署"
+msgid "Deploy Keys"
+msgstr ""
+
msgid "Description"
msgstr "æè¿°"
+msgid "Details"
+msgstr "詳情"
+
msgid "Directory name"
msgstr "目錄å稱"
+msgid "Discard changes"
+msgstr "放棄更改"
+
msgid "Don't show again"
msgstr "ä¸å†é¡¯ç¤º"
@@ -356,6 +479,27 @@ msgstr "編輯"
msgid "Edit Pipeline Schedule %{id}"
msgstr "編輯 %{id} æµæ°´ç·šè¨ˆåŠƒ"
+msgid "Emails"
+msgstr ""
+
+msgid "EventFilterBy|Filter by all"
+msgstr "全部"
+
+msgid "EventFilterBy|Filter by comments"
+msgstr "按評論 (comment) éŽæ¿¾"
+
+msgid "EventFilterBy|Filter by issue events"
+msgstr "按議題事件 (issue event) éŽæ¿¾"
+
+msgid "EventFilterBy|Filter by merge events"
+msgstr "按åˆä½µäº‹ä»¶ (merge event) éŽæ¿¾"
+
+msgid "EventFilterBy|Filter by push events"
+msgstr "按推é€äº‹ä»¶ (push event) éŽæ¿¾"
+
+msgid "EventFilterBy|Filter by team"
+msgstr "按團隊éŽæ¿¾"
+
msgid "Every day (at 4:00am)"
msgstr "æ¯æ—¥åŸ·è¡Œï¼ˆæ·©æ™¨ 4 點)"
@@ -402,35 +546,71 @@ 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 "Git 存儲å¥åº·ä¿¡æ¯å·²é‡ç½®"
+
+msgid "GitLab Runner section"
+msgstr "GitLab Runner 介紹"
+
msgid "Go to your fork"
msgstr "跳轉到派生項目"
msgid "GoToYourFork|Fork"
msgstr "跳轉到派生項目"
+msgid "Group overview"
+msgstr ""
+
+msgid "Health Check"
+msgstr "å¥åº·æª¢æŸ¥ (Health Check)"
+
+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 "Home"
msgstr "首é "
+msgid "Hooks"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "已開始維護"
msgid "Import repository"
msgstr "導入存儲庫"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr "安è£å£¹å€‹èˆ‡ GitLab CI 兼容的 Runner"
+
msgid "Interval Pattern"
msgstr "循環週期"
msgid "Introducing Cycle Analytics"
msgstr "週期分æžç°¡ä»‹"
-msgid "Jobs for last month"
-msgstr "上個月的作業"
-
-msgid "Jobs for last week"
-msgstr "上個星期的作業"
+msgid "Issue events"
+msgstr "議題事件 (issue event)"
-msgid "Jobs for last year"
-msgstr "去年的作業"
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "åœç”¨"
@@ -438,6 +618,9 @@ msgstr "åœç”¨"
msgid "LFSStatus|Enabled"
msgstr "啟用"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "最近 %d 天"
@@ -451,6 +634,12 @@ msgstr "最後更新"
msgid "Last commit"
msgstr "最後æ交"
+msgid "LastPushEvent|You pushed to"
+msgstr "您推é€äº†"
+
+msgid "LastPushEvent|at"
+msgstr "在"
+
msgid "Learn more in the"
msgstr "了解更多"
@@ -463,16 +652,40 @@ 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] "最多顯示 %d 個事件"
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "中ä½æ•¸"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr "åˆä½µäº‹ä»¶ (merge event)"
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "添加壹個 SSH 公鑰"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr "幫助文檔"
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新建議題"
@@ -570,6 +783,9 @@ msgstr "åƒèˆ‡"
msgid "NotificationLevel|Watch"
msgstr "關注"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "篩é¸"
@@ -579,9 +795,15 @@ msgstr "開始於"
msgid "Options"
msgstr "æ“作"
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
msgstr "所有者"
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
msgstr "æµæ°´ç·š"
@@ -594,6 +816,9 @@ msgstr "æµæ°´ç·šè¨ˆåŠƒ"
msgid "Pipeline Schedules"
msgstr "æµæ°´ç·šè¨ˆåŠƒ"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "失敗:"
@@ -657,6 +882,15 @@ 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 "所有"
@@ -669,6 +903,15 @@ msgstr "於階段"
msgid "Pipeline|with stages"
msgstr "於階段"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr "專案"
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "項目 '%{project_name}' 已進入刪除隊列。"
@@ -684,15 +927,16 @@ msgstr "項目 '%{project_name}' 將被刪除。"
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."
+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."
@@ -701,6 +945,12 @@ msgstr "項目導出已開始。下載éˆæŽ¥å°‡é€šéŽé›»å­éƒµä»¶ç™¼é€ã€‚"
msgid "Project home"
msgstr "項目首é "
+msgid "Project overview"
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr "訂閱"
+
msgid "ProjectFeature|Disabled"
msgstr "åœç”¨"
@@ -722,6 +972,12 @@ msgstr "階段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支圖"
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr "推é€äº‹ä»¶ (push event) "
+
msgid "Read more"
msgstr "了解更多"
@@ -758,15 +1014,30 @@ msgstr "ç¨å¾Œæ醒"
msgid "Remove project"
msgstr "刪除項目"
+msgid "Repository"
+msgstr "存儲庫"
+
msgid "Request Access"
msgstr "申請權é™"
+msgid "Reset git storage health information"
+msgstr "é‡ç½® Git 存儲的å¥åº·ä¿¡æ¯"
+
+msgid "Reset health check access token"
+msgstr "é‡ç½®å¥åº·æª¢æŸ¥è¨ªå•ä»¤ç‰Œ"
+
+msgid "Reset runners registration token"
+msgstr "é‡ç½® Runner 註冊令牌"
+
msgid "Revert this commit"
msgstr "還原此æ交"
msgid "Revert this merge request"
msgstr "還原此åˆä½µè«‹æ±‚"
+msgid "SSH Keys"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "ä¿å­˜æµæ°´ç·šè¨ˆåŠƒ"
@@ -785,9 +1056,15 @@ msgstr "é¸æ“‡ä¸‹è¼‰æ ¼å¼"
msgid "Select a timezone"
msgstr "é¸æ“‡æ™‚å€"
+msgid "Select existing branch"
+msgstr "é¸æ“‡ç¾æœ‰åˆ†æ”¯ (branch)"
+
msgid "Select target branch"
msgstr "é¸æ“‡ç›®æ¨™åˆ†æ”¯"
+msgid "Service Templates"
+msgstr ""
+
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "為賬號添加壹個用於推é€æˆ–拉å–çš„ %{protocol} 密碼。"
@@ -803,19 +1080,34 @@ msgstr "設置自動部署"
msgid "SetPasswordToCloneLink|set a password"
msgstr "設置密碼"
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "顯示 %d 個事件"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "æºä»£ç¢¼"
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr "在 Runner 設置時指定以下 URL:"
+
msgid "StarProject|Star"
msgstr "星標"
msgid "Start a %{new_merge_request} with these changes"
msgstr "由此更改 %{new_merge_request}"
+msgid "Start the Runner!"
+msgstr "é‹ä½œ Runner!"
+
msgid "Switch branch/tag"
msgstr "切æ›åˆ†æ”¯/標籤"
@@ -829,10 +1121,10 @@ msgstr "標籤"
msgid "Target Branch"
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."
+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."
@@ -841,31 +1133,19 @@ 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."
+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."
+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."
+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."
+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."
@@ -877,36 +1157,25 @@ 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."
+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."
+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."
+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 "測試階段概述了 GitLab CI 為相關åˆä½µè«‹æ±‚é‹è¡Œæ¯å€‹æµæ°´ç·šæ‰€éœ€çš„時間。當第壹個æµæ°´ç·šé‹è¡Œå®Œæˆå¾Œï¼Œæ•¸æ“šå°‡è‡ªå‹•æ·»åŠ åˆ°æ­¤è™•ã€‚"
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."
+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 "中ä½æ•¸æ˜¯å£¹å€‹æ•¸åˆ—中最中間的值。例如在 3ã€5ã€9 之間,中ä½æ•¸æ˜¯ 5。在 3ã€5ã€7ã€8 之間,中ä½æ•¸æ˜¯ (5 + 7)/ 2 = 6。"
-msgid ""
-"This means you can not push code until you create an empty repository or "
-"import existing one."
+msgid "There are problems accessing Git storage: "
+msgstr "è¨ªå• Git 存儲時出ç¾å•é¡Œï¼š"
+
+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"
@@ -1073,6 +1342,9 @@ msgstr "上傳文件"
msgid "UploadLink|click to upload"
msgstr "點擊上傳"
+msgid "Use the following registration token during setup:"
+msgstr "在安è£éŽç¨‹ä¸­ä½¿ç”¨ä»¥ä¸‹è¨»å†Šä»¤ç‰Œï¼š"
+
msgid "Use your global notification setting"
msgstr "使用全局通知設置"
@@ -1097,31 +1369,22 @@ 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}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr "å³å°‡åˆªé™¤ %{group_name}。\n"
-"已刪除的群組無法æ¢å¾©ï¼\n"
-"確定繼續嗎?"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "å³å°‡åˆªé™¤ %{group_name}。已刪除的群組無法æ¢å¾©ï¼ç¢ºå®šç¹¼çºŒå—Žï¼Ÿ"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr "å³å°‡è¦åˆªé™¤ %{project_name_with_namespace}。已刪除的項目無法æ¢è¤‡ï¼ç¢ºå®šç¹¼çºŒå—Žï¼Ÿ"
-msgid ""
-"You are going to remove the fork relationship to source project "
-"%{forked_from_project}. Are you ABSOLUTELY sure?"
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
msgstr "å³å°‡åˆªé™¤èˆ‡æºé …ç›® %{forked_from_project} 的派生關系。確定繼續嗎?"
-msgid ""
-"You are going to transfer %{project_name_with_namespace} to another owner. "
-"Are you ABSOLUTELY sure?"
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
msgstr "å³å°‡ %{project_name_with_namespace} 轉義給å¦å£¹å€‹æ‰€æœ‰è€…。確定繼續嗎?"
msgid "You can only add files when you are on a branch"
@@ -1142,26 +1405,19 @@ 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"
+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"
+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"
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
msgstr "在賬號上 %{set_password_link} 之å‰å°‡ç„¡æ³•é€šéŽ %{protocol} 拉å–或推é€ä»£ç¢¼ã€‚"
-msgid ""
-"You won't be able to pull or push project code via SSH until you "
-"%{add_ssh_key_link} to your profile"
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "在賬號中 %{add_ssh_key_link} 之å‰å°‡ç„¡æ³•é€šéŽ SSH 拉å–或推é€ä»£ç¢¼ã€‚"
msgid "Your name"
@@ -1180,3 +1436,4 @@ msgstr "通知郵件"
msgid "parent"
msgid_plural "parents"
msgstr[0] "父級"
+
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index 4fd728659c6..09c07a83d34 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -1,39 +1,51 @@
-# Huang Tao <htve@outlook.com>, 2017. #zanata
-# Hazel Yang <anonymous@domain.com>, 2017.
-# TzeKei Lee <anonymous@domain.com>, 2017.
-# Jerry Ho <a29988122@gmail.com>, 2017.
-# Lin Jen-Shin <godfat@godfat.org>, 2017. #zanata
msgid ""
msgstr ""
-"Project-Id-Version: gitlab 1.0.0\n"
+"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-05 08:50-0500\n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Chinese Traditional\n"
+"Language: zh_TW\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Language-Team: Chinese (Taiwan) (https://translate.zanata.org/project/view/GitLab)\n"
-"PO-Revision-Date: 2017-08-07 03:30-0400\n"
-"Last-Translator: Huang Tao <htve@outlook.com>\n"
-"Language: zh-TW\n"
-"X-Generator: Zanata 3.9.6\n"
-"Plural-Forms: nplurals=1; plural=0\n"
-
-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 個更動 (commit)。"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: zh-TW\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
msgid "%d commit"
msgid_plural "%d commits"
msgstr[0] "%d 個更動 (commit)"
+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 個更動 (commit)。"
+
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."
+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 "已失敗 %{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 ""
+
+msgid "%{storage_name}: failed storage access attempt on host:"
+msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
+msgstr[0] "%{storage_name}:已存å–此主機失敗 %{failed_attempts} 次"
+
+msgid "(checkout the %{link} for information on how to install it)."
+msgstr "(如何安è£è«‹åƒé–± %{link})"
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "1 æ¢æµæ°´ç·š"
-msgstr[1] "%d æ¢æµæ°´ç·š"
+msgstr[0] "%d æ¢æµæ°´ç·š"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "æŒçºŒæ•´åˆ (CI) 相關的圖表"
@@ -41,6 +53,18 @@ msgstr "æŒçºŒæ•´åˆ (CI) 相關的圖表"
msgid "About auto deploy"
msgstr "關於自動部署"
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+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 "已暫時åœç”¨å¤±æ•—çš„ Git 儲存空間。當儲存空間æ¢å¾©æ­£å¸¸å¾Œï¼Œè«‹é‡ç½®å„²å­˜ç©ºé–“å¥åº·æŒ‡æ•¸ã€‚"
+
+msgid "Account"
+msgstr ""
+
msgid "Active"
msgstr "啟用"
@@ -62,26 +86,99 @@ msgstr "請先新增 SSH 金鑰到您的個人帳號,æ‰èƒ½ä½¿ç”¨ SSH 來上å‚
msgid "Add new directory"
msgstr "新增目錄"
+msgid "All"
+msgstr "全部"
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr "此專案已å°å­˜ï¼æª”案庫 (repository) 為唯讀狀態"
msgid "Are you sure you want to delete this pipeline schedule?"
msgstr "確定è¦åˆªé™¤æ­¤æµæ°´ç·š (pipeline) 排程嗎?"
+msgid "Are you sure you want to discard your changes?"
+msgstr "確定è¦æ”¾æ£„修改嗎?"
+
+msgid "Are you sure you want to reset registration token?"
+msgstr "確定è¦é‡ç½®è¨»å†Šæ†‘è­‰ (registration token) 嗎?"
+
+msgid "Are you sure you want to reset the health check token?"
+msgstr "確定è¦é‡ç½®å¥åº·æª¢æŸ¥å­˜å–憑證 (access token) 嗎?"
+
+msgid "Are you sure?"
+msgstr "確定嗎?"
+
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放檔案到此處或者 %{upload_link}"
+msgid "Authentication log"
+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 "Branch"
msgid_plural "Branches"
msgstr[0] "分支 (branch) "
-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> 。如è¦è¨­å®šè‡ªå‹•éƒ¨ç½²ï¼Œ è«‹é¸æ“‡åˆé©çš„ GitLab CI "
-"Yaml 模æ¿ï¼Œç„¶å¾Œè¨˜å¾—è¦é€äº¤ (commit) 您的編輯內容。%{link_to_autodeploy_doc}\n"
+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> 。如è¦è¨­å®šè‡ªå‹•éƒ¨ç½²ï¼Œ è«‹é¸æ“‡åˆé©çš„ GitLab CI Yaml 模æ¿ï¼Œç„¶å¾Œè¨˜å¾—è¦é€äº¤ (commit) 您的編輯內容。%{link_to_autodeploy_doc}\n"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "æœå°‹åˆ†æ”¯ (branches)"
@@ -107,12 +204,18 @@ msgstr "ç€è¦½æª”案"
msgid "ByAuthor|by"
msgstr "作者:"
+msgid "CI / CD"
+msgstr ""
+
msgid "CI configuration"
msgstr "CI 組態"
msgid "Cancel"
msgstr "å–消"
+msgid "Cancel edit"
+msgstr "å–消編輯"
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "挑é¸åˆ°åˆ†æ”¯ (branch) "
@@ -131,6 +234,9 @@ msgstr "更新日誌"
msgid "Charts"
msgstr "統計圖"
+msgid "Chat"
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr "挑é¸æ­¤æ›´å‹•è¨˜éŒ„ (commit) "
@@ -191,6 +297,9 @@ msgstr "已跳éŽ"
msgid "CiStatus|running"
msgstr "執行中"
+msgid "Comments"
+msgstr "留言"
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "更動記錄 (commit) "
@@ -222,12 +331,18 @@ msgstr "é€äº¤è€…為 "
msgid "Compare"
msgstr "比較"
+msgid "Container Registry"
+msgstr ""
+
msgid "Contribution guide"
msgstr "å”作指å—"
msgid "Contributors"
msgstr "å”作者"
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr "複製網å€åˆ°å‰ªè²¼ç°¿"
@@ -237,9 +352,10 @@ msgstr "複製更動記錄 (commit) 的 SHA 值到剪貼簿"
msgid "Create New Directory"
msgstr "建立新目錄"
-msgid ""
-"Create a personal access token on your account to pull or push via "
-"%{protocol}."
+msgid "Create a new branch"
+msgstr "建立新分支 (branch)"
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
msgstr "建立個人存å–憑證 (access token) 以使用 %{protocol} 來上傳 (push) 或下載 (pull) 。"
msgid "Create directory"
@@ -272,19 +388,13 @@ msgstr "Cron 語法"
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 ""
-"自訂通知層級相當於åƒèˆ‡åº¦è¨­å®šã€‚使用自訂通知層級,您å¯ä»¥åªæ”¶åˆ°ç‰¹å®šçš„事件通知。請åƒç…§ %{notification_link} 以ç²å¾—更多訊æ¯ã€‚"
+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 "自訂通知層級相當於åƒèˆ‡åº¦è¨­å®šã€‚使用自訂通知層級,您å¯ä»¥åªæ”¶åˆ°ç‰¹å®šçš„事件通知。請åƒç…§ %{notification_link} 以ç²å¾—更多訊æ¯ã€‚"
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."
+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"
@@ -318,12 +428,21 @@ msgid "Deploy"
msgid_plural "Deploys"
msgstr[0] "部署"
+msgid "Deploy Keys"
+msgstr ""
+
msgid "Description"
msgstr "æè¿°"
+msgid "Details"
+msgstr "細節"
+
msgid "Directory name"
msgstr "目錄å稱"
+msgid "Discard changes"
+msgstr "放棄修改"
+
msgid "Don't show again"
msgstr "ä¸å†é¡¯ç¤º"
@@ -360,6 +479,27 @@ msgstr "編輯"
msgid "Edit Pipeline Schedule %{id}"
msgstr "編輯 %{id} æµæ°´ç·š (pipeline) 排程"
+msgid "Emails"
+msgstr ""
+
+msgid "EventFilterBy|Filter by all"
+msgstr "顯示全部"
+
+msgid "EventFilterBy|Filter by comments"
+msgstr "以留言篩é¸"
+
+msgid "EventFilterBy|Filter by issue events"
+msgstr "以議題 (issue) 事件篩é¸"
+
+msgid "EventFilterBy|Filter by merge events"
+msgstr "以åˆä½µ (merge) 事件篩é¸"
+
+msgid "EventFilterBy|Filter by push events"
+msgstr "ä»¥æŽ¨é€ (push) 事件篩é¸"
+
+msgid "EventFilterBy|Filter by team"
+msgstr "以團隊篩é¸"
+
msgid "Every day (at 4:00am)"
msgstr "æ¯æ—¥åŸ·è¡Œï¼ˆæ·©æ™¨å››é»žï¼‰"
@@ -406,35 +546,71 @@ msgstr "從議題 (issue) 建立直到部署至營é‹ç’°å¢ƒ"
msgid "From merge request merge until deploy to production"
msgstr "從請求被åˆä½µå¾Œ (merge request merged) 直到部署至營é‹ç’°å¢ƒ"
+msgid "GPG Keys"
+msgstr ""
+
+msgid "Geo Nodes"
+msgstr ""
+
+msgid "Git storage health information has been reset"
+msgstr "Git 儲存空間å¥åº·æŒ‡æ•¸å·²é‡ç½®"
+
+msgid "GitLab Runner section"
+msgstr "GitLab Runner"
+
msgid "Go to your fork"
msgstr "å‰å¾€æ‚¨çš„分支 (fork) "
msgid "GoToYourFork|Fork"
msgstr "å‰å¾€æ‚¨çš„分支 (fork) "
+msgid "Group overview"
+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 "å­˜å–憑證 (access token) 是"
+
+msgid "HealthCheck|Healthy"
+msgstr "å¥åº·"
+
+msgid "HealthCheck|No Health Problems Detected"
+msgstr "沒有檢測到å¥åº·å•é¡Œ"
+
+msgid "HealthCheck|Unhealthy"
+msgstr "ä¸è‰¯"
+
msgid "Home"
msgstr "首é "
+msgid "Hooks"
+msgstr ""
+
msgid "Housekeeping successfully started"
msgstr "已開始維護"
msgid "Import repository"
msgstr "匯入檔案庫 (repository)"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr "安è£èˆ‡ GitLab CI 相容的 Runner"
+
msgid "Interval Pattern"
msgstr "循環週期"
msgid "Introducing Cycle Analytics"
msgstr "週期分æžç°¡ä»‹"
-msgid "Jobs for last month"
-msgstr "上個月的任務 (job) "
-
-msgid "Jobs for last week"
-msgstr "上個星期的任務 (job) "
+msgid "Issue events"
+msgstr "議題 (issue) 事件"
-msgid "Jobs for last year"
-msgstr "去年的任務 (job) "
+msgid "Issues"
+msgstr ""
msgid "LFSStatus|Disabled"
msgstr "åœç”¨"
@@ -442,6 +618,9 @@ msgstr "åœç”¨"
msgid "LFSStatus|Enabled"
msgstr "啟用"
+msgid "Labels"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "最近 %d 天"
@@ -455,6 +634,12 @@ msgstr "最後更新"
msgid "Last commit"
msgstr "最後更動記錄 (commit) "
+msgid "LastPushEvent|You pushed to"
+msgstr "您上傳 (push) 了"
+
+msgid "LastPushEvent|at"
+msgstr "æ–¼"
+
msgid "Learn more in the"
msgstr "了解更多"
@@ -467,16 +652,40 @@ 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] "é™åˆ¶æœ€å¤šé¡¯ç¤º %d 個事件"
+msgid "Locked Files"
+msgstr ""
+
msgid "Median"
msgstr "中ä½æ•¸"
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr "åˆä½µ (merge) 事件"
+
+msgid "Messages"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "新增 SSH 金鑰"
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr "å¥åº·æª¢æŸ¥"
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "建立議題 (issue) "
@@ -574,6 +783,9 @@ msgstr "åƒèˆ‡"
msgid "NotificationLevel|Watch"
msgstr "關注"
+msgid "Notifications"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr "篩é¸"
@@ -583,9 +795,15 @@ msgstr "開始於"
msgid "Options"
msgstr "é¸é …"
+msgid "Overview"
+msgstr ""
+
msgid "Owner"
msgstr "所有權"
+msgid "Password"
+msgstr ""
+
msgid "Pipeline"
msgstr "æµæ°´ç·š (pipeline) "
@@ -598,6 +816,9 @@ msgstr "æµæ°´ç·š (pipeline) 排程"
msgid "Pipeline Schedules"
msgstr "æµæ°´ç·š (pipeline) 排程"
+msgid "Pipeline quota"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "失敗:"
@@ -661,6 +882,15 @@ msgstr "æµæ°´ç·š (pipeline) "
msgid "Pipelines charts"
msgstr "æµæ°´ç·š (pipeline) 圖表"
+msgid "Pipelines for last month"
+msgstr ""
+
+msgid "Pipelines for last week"
+msgstr ""
+
+msgid "Pipelines for last year"
+msgstr ""
+
msgid "Pipeline|all"
msgstr "所有"
@@ -673,6 +903,15 @@ msgstr "於階段"
msgid "Pipeline|with stages"
msgstr "於階段"
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr "專案"
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "專案 '%{project_name}' 已加入刪除佇列。"
@@ -688,15 +927,16 @@ msgstr "專案 '%{project_name}' 將被刪除。"
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."
+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."
@@ -705,6 +945,12 @@ msgstr "專案導出已開始。完æˆå¾Œä¸‹è¼‰é€£çµæœƒé€åˆ°æ‚¨çš„信箱。"
msgid "Project home"
msgstr "專案首é "
+msgid "Project overview"
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr "訂閱"
+
msgid "ProjectFeature|Disabled"
msgstr "åœç”¨"
@@ -726,6 +972,12 @@ msgstr "階段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支圖"
+msgid "Push Rules"
+msgstr ""
+
+msgid "Push events"
+msgstr "æŽ¨é€ (push) 事件"
+
msgid "Read more"
msgstr "瞭解更多"
@@ -762,15 +1014,30 @@ msgstr "ç¨å¾Œæ醒"
msgid "Remove project"
msgstr "刪除專案"
+msgid "Repository"
+msgstr "檔案庫 (repository)"
+
msgid "Request Access"
msgstr "申請權é™"
+msgid "Reset git storage health information"
+msgstr "é‡ç½® Git 儲存空間å¥åº·æŒ‡æ•¸"
+
+msgid "Reset health check access token"
+msgstr "é‡ç½®å¥åº·æª¢æŸ¥å­˜å–憑證 (access token)"
+
+msgid "Reset runners registration token"
+msgstr "é‡ç½® Runner 註冊憑證 (registration token)"
+
msgid "Revert this commit"
msgstr "還原此更動記錄 (commit)"
msgid "Revert this merge request"
msgstr "還原此åˆä½µè«‹æ±‚ (merge request) "
+msgid "SSH Keys"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr "儲存æµæ°´ç·š (pipeline) 排程"
@@ -789,9 +1056,15 @@ msgstr "é¸æ“‡ä¸‹è¼‰æ ¼å¼"
msgid "Select a timezone"
msgstr "é¸æ“‡æ™‚å€"
+msgid "Select existing branch"
+msgstr "é¸æ“‡ç¾æœ‰åˆ†æ”¯ (branch)"
+
msgid "Select target branch"
msgstr "é¸æ“‡ç›®æ¨™åˆ†æ”¯ (branch) "
+msgid "Service Templates"
+msgstr ""
+
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "請先設定密碼,æ‰èƒ½ä½¿ç”¨ %{protocol} 來上傳 (push) 或下載 (pull) 。"
@@ -807,19 +1080,34 @@ msgstr "設定自動部署"
msgid "SetPasswordToCloneLink|set a password"
msgstr "設定密碼"
+msgid "Settings"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "顯示 %d 個事件"
+msgid "Snippets"
+msgstr ""
+
msgid "Source code"
msgstr "原始碼"
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr "åœ¨å®‰è£ Runner 時指定以下 URL:"
+
msgid "StarProject|Star"
msgstr "收è—"
msgid "Start a %{new_merge_request} with these changes"
msgstr "以這些改動建立一個新的 %{new_merge_request} "
+msgid "Start the Runner!"
+msgstr "å•Ÿå‹• Runner!"
+
msgid "Switch branch/tag"
msgstr "切æ›åˆ†æ”¯ (branch) 或標籤"
@@ -833,12 +1121,11 @@ msgstr "標籤"
msgid "Target Branch"
msgstr "目標分支 (branch) "
-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 ""
-"程å¼é–‹ç™¼éšŽæ®µé¡¯ç¤ºå¾žç¬¬ä¸€æ¬¡æ›´å‹•è¨˜éŒ„ (commit) 到建立åˆä½µè«‹æ±‚ (merge request) 的時間。建立第一個åˆä½µè«‹æ±‚後,資料將自動填入。"
+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 "程å¼é–‹ç™¼éšŽæ®µé¡¯ç¤ºå¾žç¬¬ä¸€æ¬¡æ›´å‹•è¨˜éŒ„ (commit) 到建立åˆä½µè«‹æ±‚ (merge request) 的時間。建立第一個åˆä½µè«‹æ±‚後,資料將自動填入。"
msgid "The collection of events added to the data gathered for that stage."
msgstr "該階段中的相關事件集åˆã€‚"
@@ -846,34 +1133,19 @@ msgstr "該階段中的相關事件集åˆã€‚"
msgid "The fork relationship has been removed."
msgstr "åˆ†æ”¯èˆ‡ä¸»å¹¹é–“çš„é—œè¯ (fork relationship) 已被刪除。"
-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 ""
-"è­°é¡Œ (issue) éšŽæ®µé¡¯ç¤ºå¾žè­°é¡Œå»ºç«‹åˆ°è¨­å®šé‡Œç¨‹ç¢‘æ‰€èŠ±çš„æ™‚é–“ï¼Œæˆ–æ˜¯è­°é¡Œè¢«åˆ†é¡žåˆ°è­°é¡Œçœ‹æ¿ (issue board) "
-"中所花的時間。建立第一個議題後,資料將自動填入。"
+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 "è­°é¡Œ (issue) éšŽæ®µé¡¯ç¤ºå¾žè­°é¡Œå»ºç«‹åˆ°è¨­å®šé‡Œç¨‹ç¢‘æ‰€èŠ±çš„æ™‚é–“ï¼Œæˆ–æ˜¯è­°é¡Œè¢«åˆ†é¡žåˆ°è­°é¡Œçœ‹æ¿ (issue board) 中所花的時間。建立第一個議題後,資料將自動填入。"
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 ""
-"在指定了特定分支 (branch) 或標籤後,此處的æµæ°´ç·š (pipeline) 排程會ä¸æ–·åœ°é‡è¤‡åŸ·è¡Œã€‚æµæ°´ç·šæŽ’程的存å–權é™èˆ‡å°ˆæ¡ˆæœ¬èº«ç›¸åŒã€‚"
+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 "在指定了特定分支 (branch) 或標籤後,此處的æµæ°´ç·š (pipeline) 排程會ä¸æ–·åœ°é‡è¤‡åŸ·è¡Œã€‚æµæ°´ç·šæŽ’程的存å–權é™èˆ‡å°ˆæ¡ˆæœ¬èº«ç›¸åŒã€‚"
-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."
+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 "計劃階段顯示從更動記錄 (commit) 被排程至第一個推é€çš„時間。第一次推é€ä¹‹å¾Œï¼Œè³‡æ–™å°‡è‡ªå‹•å¡«å…¥ã€‚"
-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."
+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 "營é‹éšŽæ®µé¡¯ç¤ºå¾žå»ºç«‹è­°é¡Œ (issue) 到部署程å¼ä¸Šç·šæ‰€èŠ±çš„時間。完æˆå¾žç™¼æƒ³åˆ°ä¸Šç·šçš„完整開發週期後,資料將自動填入。"
msgid "The project can be accessed by any logged in user."
@@ -885,39 +1157,25 @@ msgstr "本專案å¯è®“任何人存å–"
msgid "The repository for this project does not exist."
msgstr "本專案沒有檔案庫 (repository) "
-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 ""
-"複閱階段顯示從åˆä½µè«‹æ±‚ (merge request) 建立後至被åˆä½µçš„時間。當建立第一個åˆä½µè«‹æ±‚ (merge request) 後,資料將自動填入。"
+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 "複閱階段顯示從åˆä½µè«‹æ±‚ (merge request) 建立後至被åˆä½µçš„時間。當建立第一個åˆä½µè«‹æ±‚ (merge request) 後,資料將自動填入。"
-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."
+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 "試營é‹æ®µé¡¯ç¤ºå¾žåˆä½µè«‹æ±‚ (merge request) 被åˆä½µå¾Œè‡³éƒ¨ç½²ç‡Ÿé‹çš„時間。當第一次部署營é‹å¾Œï¼Œè³‡æ–™å°‡è‡ªå‹•å¡«å…¥"
-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 ""
-"測試階段顯示相關åˆä½µè«‹æ±‚ (merge request) çš„æµæ°´ç·š (pipeline) 所花的時間。當第一個æµæ°´ç·š (pipeline) "
-"執行完畢後,資料將自動填入。"
+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 "測試階段顯示相關åˆä½µè«‹æ±‚ (merge request) çš„æµæ°´ç·š (pipeline) 所花的時間。當第一個æµæ°´ç·š (pipeline) 執行完畢後,資料將自動填入。"
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."
+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 "中ä½æ•¸æ˜¯ä¸€å€‹æ•¸åˆ—中最中間的值。例如在 3ã€5ã€9 之間,中ä½æ•¸æ˜¯ 5。在 3ã€5ã€7ã€8 之間,中ä½æ•¸æ˜¯ (5 + 7)/ 2 = 6。"
-msgid ""
-"This means you can not push code until you create an empty repository or "
-"import existing one."
+msgid "There are problems accessing Git storage: "
+msgstr "å­˜å– Git 儲存空間時出ç¾å•é¡Œï¼š"
+
+msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr "這代表在您建立一個空的檔案庫 (repository) 或是匯入一個ç¾å­˜çš„檔案庫之å‰ï¼Œæ‚¨å°‡ç„¡æ³•ä¸Šå‚³æ›´æ–° (push) 。"
msgid "Time before an issue gets scheduled"
@@ -1084,6 +1342,9 @@ msgstr "上傳檔案"
msgid "UploadLink|click to upload"
msgstr "點擊上傳"
+msgid "Use the following registration token during setup:"
+msgstr "在安è£éŽç¨‹ä¸­ä½¿ç”¨æ­¤è¨»å†Šæ†‘è­‰ (registration token):"
+
msgid "Use your global notification setting"
msgstr "使用全域通知設定"
@@ -1108,34 +1369,23 @@ 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}.\n"
-"Removed groups CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr "å³å°‡è¦åˆªé™¤ %{group_name}。\n"
-"被刪除的群組完全無法救回來喔ï¼\n"
-"真的「100%確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "å°‡è¦åˆªé™¤ %{group_name}。被刪除的群組無法復原ï¼çœŸçš„「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
-msgid ""
-"You are going to remove %{project_name_with_namespace}.\n"
-"Removed project CANNOT be restored!\n"
-"Are you ABSOLUTELY sure?"
-msgstr "å³å°‡è¦åˆªé™¤ %{project_name_with_namespace}。被刪除的專案完全無法救回來喔ï¼çœŸçš„「100%確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "å°‡è¦åˆªé™¤ %{project_name_with_namespace}。被刪除的專案無法復原ï¼çœŸçš„「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
-msgid ""
-"You are going to remove the fork relationship to source project "
-"%{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr ""
-"å°‡è¦åˆªé™¤æœ¬åˆ†æ”¯å°ˆæ¡ˆèˆ‡ä¸»å¹¹çš„æ‰€æœ‰é—œè¯ (fork relationship) 。 %{forked_from_project} "
-"真的「100%確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr "å°‡è¦åˆªé™¤æœ¬åˆ†æ”¯å°ˆæ¡ˆèˆ‡ä¸»å¹¹ %{forked_from_project} 的所有關è¯ã€‚ 真的「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
-msgid ""
-"You are going to transfer %{project_name_with_namespace} to another owner. "
-"Are you ABSOLUTELY sure?"
-msgstr "å°‡è¦æŠŠ %{project_name_with_namespace} 的所有權轉移給å¦ä¸€å€‹äººã€‚真的「100%確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr "å°‡è¦æŠŠ %{project_name_with_namespace} 的所有權轉移給å¦ä¸€å€‹äººã€‚真的「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
msgid "You can only add files when you are on a branch"
msgstr "åªèƒ½åœ¨åˆ†æ”¯ (branch) 上建立檔案"
@@ -1155,27 +1405,19 @@ 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"
+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"
+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 ""
-"在帳號上 %{set_password_link} 之å‰ï¼Œ 將無法使用 %{protocol} 上傳 (push) 或下載 (pull) 程å¼ç¢¼ã€‚"
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr "在帳號上 %{set_password_link} 之å‰ï¼Œ 將無法使用 %{protocol} 上傳 (push) 或下載 (pull) 程å¼ç¢¼ã€‚"
-msgid ""
-"You won't be able to pull or push project code via SSH until you "
-"%{add_ssh_key_link} to your profile"
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "在個人帳號中 %{add_ssh_key_link} 之å‰ï¼Œ 將無法使用 SSH 上傳 (push) 或下載 (pull) 程å¼ç¢¼ã€‚"
msgid "Your name"
@@ -1194,3 +1436,4 @@ msgstr "通知信"
msgid "parent"
msgid_plural "parents"
msgstr[0] "上層"
+
diff --git a/package.json b/package.json
index c5247a63e67..feae6ca9748 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,8 @@
"babel-preset-latest": "^6.24.0",
"babel-preset-stage-2": "^6.22.0",
"bootstrap-sass": "^3.3.6",
- "compression-webpack-plugin": "^0.3.2",
+ "brace-expansion": "^1.1.8",
+ "compression-webpack-plugin": "^1.0.0",
"copy-webpack-plugin": "^4.0.1",
"core-js": "^2.4.1",
"cropper": "^2.3.0",
@@ -63,7 +64,8 @@
"vue-loader": "^11.3.4",
"vue-resource": "^1.3.4",
"vue-template-compiler": "^2.2.6",
- "webpack": "^3.4.0",
+ "vuex": "^2.3.1",
+ "webpack": "^3.5.5",
"webpack-bundle-analyzer": "^2.8.2",
"webpack-stats-plugin": "^0.1.5"
},
diff --git a/public/404.html b/public/404.html
index 03e98e81862..08f328da542 100644
--- a/public/404.html
+++ b/public/404.html
@@ -15,7 +15,7 @@
h1 {
font-size: 56px;
line-height: 100px;
- font-weight: normal;
+ font-weight: 400;
color: #456;
}
@@ -28,7 +28,7 @@
h3 {
color: #456;
font-size: 20px;
- font-weight: normal;
+ font-weight: 400;
line-height: 28px;
}
@@ -48,7 +48,7 @@
a {
line-height: 100px;
- font-weight: normal;
+ font-weight: 400;
color: #4A8BEE;
font-size: 18px;
text-decoration: none;
@@ -72,8 +72,9 @@
404
</h1>
<div class="container">
- <h3>The page you're looking for could not be found.</h3>
+ <h3>The page could not be found or you don't have permission to view it.</h3>
<hr />
+ <p>The resource that you are attempting to access does not exist or you don't have the necessary permissions to view it.</p>
<p>Make sure the address is correct and that the page hasn't moved.</p>
<p>Please contact your GitLab administrator if you think this is a mistake.</p>
<a href="javascript:history.back()" class="js-go-back go-back">Go back</a>
diff --git a/public/422.html b/public/422.html
index 49ebbe40f39..a67dcd02200 100644
--- a/public/422.html
+++ b/public/422.html
@@ -15,7 +15,7 @@
h1 {
font-size: 56px;
line-height: 100px;
- font-weight: normal;
+ font-weight: 400;
color: #456;
}
@@ -28,7 +28,7 @@
h3 {
color: #456;
font-size: 20px;
- font-weight: normal;
+ font-weight: 400;
line-height: 28px;
}
@@ -48,7 +48,7 @@
a {
line-height: 100px;
- font-weight: normal;
+ font-weight: 400;
color: #4A8BEE;
font-size: 18px;
text-decoration: none;
diff --git a/public/500.html b/public/500.html
index 516920f7471..7091d14dfc4 100644
--- a/public/500.html
+++ b/public/500.html
@@ -15,7 +15,7 @@
h1 {
font-size: 56px;
line-height: 100px;
- font-weight: normal;
+ font-weight: 400;
color: #456;
}
@@ -28,7 +28,7 @@
h3 {
color: #456;
font-size: 20px;
- font-weight: normal;
+ font-weight: 400;
line-height: 28px;
}
@@ -48,7 +48,7 @@
a {
line-height: 100px;
- font-weight: normal;
+ font-weight: 400;
color: #4A8BEE;
font-size: 18px;
text-decoration: none;
diff --git a/public/502.html b/public/502.html
index 189458c9816..82afd273248 100644
--- a/public/502.html
+++ b/public/502.html
@@ -15,7 +15,7 @@
h1 {
font-size: 56px;
line-height: 100px;
- font-weight: normal;
+ font-weight: 400;
color: #456;
}
@@ -28,7 +28,7 @@
h3 {
color: #456;
font-size: 20px;
- font-weight: normal;
+ font-weight: 400;
line-height: 28px;
}
@@ -48,7 +48,7 @@
a {
line-height: 100px;
- font-weight: normal;
+ font-weight: 400;
color: #4A8BEE;
font-size: 18px;
text-decoration: none;
diff --git a/public/503.html b/public/503.html
index b09b0e2a67e..f1486bc3e84 100644
--- a/public/503.html
+++ b/public/503.html
@@ -15,7 +15,7 @@
h1 {
font-size: 56px;
line-height: 100px;
- font-weight: normal;
+ font-weight: 400;
color: #456;
}
@@ -28,7 +28,7 @@
h3 {
color: #456;
font-size: 20px;
- font-weight: normal;
+ font-weight: 400;
line-height: 28px;
}
@@ -48,7 +48,7 @@
a {
line-height: 100px;
- font-weight: normal;
+ font-weight: 400;
color: #4A8BEE;
font-size: 18px;
text-decoration: none;
diff --git a/public/deploy.html b/public/deploy.html
index 49ec4ac5ce1..e463b62520c 100644
--- a/public/deploy.html
+++ b/public/deploy.html
@@ -20,7 +20,7 @@
h1 {
font-size: 56px;
line-height: 100px;
- font-weight: normal;
+ font-weight: 400;
color: #456;
}
@@ -33,7 +33,7 @@
h3 {
color: #456;
font-size: 20px;
- font-weight: normal;
+ font-weight: 400;
line-height: 28px;
}
@@ -66,4 +66,4 @@
<p>Please contact your GitLab administrator if this problem persists.</p>
</div>
</body>
-</html> \ No newline at end of file
+</html>
diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb
index 4f83a773645..12e56404cf6 100644
--- a/qa/qa/runtime/release.rb
+++ b/qa/qa/runtime/release.rb
@@ -21,7 +21,7 @@ module QA
end
def self.method_missing(name, *args)
- self.new.strategy.public_send(name, *args)
+ self.new.strategy.public_send(name, *args) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/rubocop/cop/migration/safer_boolean_column.rb b/rubocop/cop/migration/safer_boolean_column.rb
new file mode 100644
index 00000000000..0335c25d85d
--- /dev/null
+++ b/rubocop/cop/migration/safer_boolean_column.rb
@@ -0,0 +1,94 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # This cop requires a default value and disallows nulls for boolean
+ # columns on small tables.
+ #
+ # In general, this prevents 3-state-booleans.
+ # https://robots.thoughtbot.com/avoid-the-threestate-boolean-problem
+ #
+ # In particular, for the `application_settings` table, this ensures that
+ # upgraded installations get a proper default for the new boolean setting.
+ # A developer might otherwise mistakenly assume that a value in
+ # `ApplicationSetting.defaults` is sufficient.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab-ee/issues/2750 for more
+ # information.
+ class SaferBooleanColumn < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ DEFAULT_OFFENSE = 'Boolean columns on the `%s` table should have a default. You may wish to use `add_column_with_default`.'.freeze
+ NULL_OFFENSE = 'Boolean columns on the `%s` table should disallow nulls.'.freeze
+ DEFAULT_AND_NULL_OFFENSE = 'Boolean columns on the `%s` table should have a default and should disallow nulls. You may wish to use `add_column_with_default`.'.freeze
+
+ SMALL_TABLES = %i[
+ application_settings
+ ].freeze
+
+ def_node_matcher :add_column?, <<~PATTERN
+ (send nil :add_column $...)
+ PATTERN
+
+ def on_send(node)
+ return unless in_migration?(node)
+
+ matched = add_column?(node)
+
+ return unless matched
+
+ table, _, type = matched.to_a.take(3).map(&:children).map(&:first)
+ opts = matched[3]
+
+ return unless SMALL_TABLES.include?(table) && type == :boolean
+
+ no_default = no_default?(opts)
+ nulls_allowed = nulls_allowed?(opts)
+
+ offense = if no_default && nulls_allowed
+ DEFAULT_AND_NULL_OFFENSE
+ elsif no_default
+ DEFAULT_OFFENSE
+ elsif nulls_allowed
+ NULL_OFFENSE
+ end
+
+ add_offense(node, :expression, format(offense, table)) if offense
+ end
+
+ def no_default?(opts)
+ return true unless opts
+
+ each_hash_node_pair(opts) do |key, value|
+ return value == 'nil' if key == :default
+ end
+ end
+
+ def nulls_allowed?(opts)
+ return true unless opts
+
+ each_hash_node_pair(opts) do |key, value|
+ return value != 'false' if key == :null
+ end
+ end
+
+ def each_hash_node_pair(hash_node, &block)
+ hash_node.each_node(:pair) do |pair|
+ key = hash_pair_key(pair)
+ value = hash_pair_value(pair)
+ yield(key, value)
+ end
+ end
+
+ def hash_pair_key(pair)
+ pair.children[0].children[0]
+ end
+
+ def hash_pair_value(pair)
+ pair.children[1].source
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index 3fbd5b0163c..1b6e8991a17 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -13,6 +13,7 @@ require_relative 'cop/migration/add_concurrent_index'
require_relative 'cop/migration/add_index'
require_relative 'cop/migration/add_timestamps'
require_relative 'cop/migration/datetime'
+require_relative 'cop/migration/safer_boolean_column'
require_relative 'cop/migration/hash_index'
require_relative 'cop/migration/remove_concurrent_index'
require_relative 'cop/migration/remove_index'
diff --git a/scripts/detect-new-flaky-examples b/scripts/detect-new-flaky-examples
new file mode 100755
index 00000000000..3bee4f9a34b
--- /dev/null
+++ b/scripts/detect-new-flaky-examples
@@ -0,0 +1,21 @@
+#!/usr/bin/env ruby
+
+require 'json'
+
+report_file = ARGV.shift
+unless report_file
+ puts 'usage: detect-new-flaky-examples <report-file>'
+ exit 1
+end
+
+puts "Loading #{report_file}..."
+report = JSON.parse(File.read(report_file))
+
+if report.any?
+ puts "New flaky examples were detected!\n"
+ puts JSON.pretty_generate(report)
+ exit 1
+else
+ puts "No new flaky examples detected.\n"
+ exit 0
+end
diff --git a/scripts/merge-reports b/scripts/merge-reports
index aad76bcc327..3a421f1f1fc 100755
--- a/scripts/merge-reports
+++ b/scripts/merge-reports
@@ -4,7 +4,7 @@ require 'json'
main_report_file = ARGV.shift
unless main_report_file
- puts 'usage: merge_reports <main-report> [extra reports...]'
+ puts 'usage: merge-reports <main-report> [extra reports...]'
exit 1
end
diff --git a/scripts/static-analysis b/scripts/static-analysis
index e4f80e8fc6f..295b6f132c1 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -3,7 +3,7 @@
require ::File.expand_path('../lib/gitlab/popen', __dir__)
tasks = [
- %w[bundle exec bundle-audit check --update --ignore CVE-2016-4658 CVE-2017-5029],
+ %w[bundle exec bundle-audit check --update],
%w[bundle exec rake config_lint],
%w[bundle exec rake flay],
%w[bundle exec rake haml_lint],
@@ -12,7 +12,8 @@ tasks = [
%w[bundle exec license_finder],
%w[yarn run eslint],
%w[bundle exec rubocop --require rubocop-rspec],
- %w[scripts/lint-conflicts.sh]
+ %w[scripts/lint-conflicts.sh],
+ %w[bundle exec rake gettext:lint]
]
failed_tasks = tasks.reduce({}) do |failures, task|
diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs
new file mode 100755
index 00000000000..44f832ed3e6
--- /dev/null
+++ b/scripts/trigger-build-docs
@@ -0,0 +1,80 @@
+#!/usr/bin/env ruby
+
+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|
+ config.endpoint = 'https://gitlab.com/api/v4'
+ config.private_token = ENV["DOCS_API_TOKEN"] # GitLab Docs bot access token which has only Developer access to gitlab-docs
+end
+
+#
+# Dummy way to find out in which repo we are, CE or EE
+#
+def ee?
+ File.exist?('CHANGELOG-EE.md')
+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"
+rescue Gitlab::Error::BadRequest
+ 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"
+end
+
+#
+# Trigger a pipeline in gitlab-docs
+#
+def trigger_pipeline
+ # Overriding vars in https://gitlab.com/gitlab-com/gitlab-docs/blob/master/.gitlab-ci.yml
+ param_name = ee? ? 'BRANCH_EE' : 'BRANCH_CE'
+
+ # The review app URL
+ 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"] })
+
+ puts "=> Pipeline created:"
+ puts ""
+ puts "https://gitlab.com/gitlab-com/gitlab-docs/pipelines/#{pipeline.id}"
+ puts ""
+ puts "=> Preview your changes live at:"
+ puts ""
+ puts app_url
+ puts ""
+end
+
+#
+# When the first argument is deploy then create the branch and trigger pipeline
+# When it is 'stop', it deleted the remote branch. That way, we ensure there
+# are no stale remote branches and the Review server doesn't fill.
+#
+case ARGV[0]
+when 'deploy'
+ create_remote_branch
+ trigger_pipeline
+when 'cleanup'
+ remove_remote_branch
+end
diff --git a/scripts/trigger-build b/scripts/trigger-build-omnibus
index dcda70d7ed8..dcda70d7ed8 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build-omnibus
diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb
index 91aff0db7cc..6d8b9865dcb 100644
--- a/spec/bin/changelog_spec.rb
+++ b/spec/bin/changelog_spec.rb
@@ -4,56 +4,90 @@ load File.expand_path('../../bin/changelog', __dir__)
describe 'bin/changelog' do
describe ChangelogOptionParser do
- it 'parses --ammend' do
- options = described_class.parse(%w[foo bar --amend])
+ describe '.parse' do
+ it 'parses --amend' do
+ options = described_class.parse(%w[foo bar --amend])
- expect(options.amend).to eq true
- end
+ expect(options.amend).to eq true
+ end
- it 'parses --force and -f' do
- %w[--force -f].each do |flag|
- options = described_class.parse(%W[foo #{flag} bar])
+ it 'parses --force and -f' do
+ %w[--force -f].each do |flag|
+ options = described_class.parse(%W[foo #{flag} bar])
- expect(options.force).to eq true
+ expect(options.force).to eq true
+ end
end
- end
- it 'parses --merge-request and -m' do
- %w[--merge-request -m].each do |flag|
- options = described_class.parse(%W[foo #{flag} 1234 bar])
+ it 'parses --merge-request and -m' do
+ %w[--merge-request -m].each do |flag|
+ options = described_class.parse(%W[foo #{flag} 1234 bar])
- expect(options.merge_request).to eq 1234
+ expect(options.merge_request).to eq 1234
+ end
end
- end
- it 'parses --dry-run and -n' do
- %w[--dry-run -n].each do |flag|
- options = described_class.parse(%W[foo #{flag} bar])
+ it 'parses --dry-run and -n' do
+ %w[--dry-run -n].each do |flag|
+ options = described_class.parse(%W[foo #{flag} bar])
- expect(options.dry_run).to eq true
+ expect(options.dry_run).to eq true
+ end
end
- end
- it 'parses --git-username and -u' do
- allow(described_class).to receive(:git_user_name).and_return('Jane Doe')
+ it 'parses --git-username and -u' do
+ allow(described_class).to receive(:git_user_name).and_return('Jane Doe')
- %w[--git-username -u].each do |flag|
- options = described_class.parse(%W[foo #{flag} bar])
+ %w[--git-username -u].each do |flag|
+ options = described_class.parse(%W[foo #{flag} bar])
- expect(options.author).to eq 'Jane Doe'
+ expect(options.author).to eq 'Jane Doe'
+ end
+ end
+
+ it 'parses --type and -t' do
+ %w[--type -t].each do |flag|
+ options = described_class.parse(%W[foo #{flag} security])
+
+ expect(options.type).to eq 'security'
+ end
end
- end
- it 'parses -h' do
- expect do
- expect { described_class.parse(%w[foo -h bar]) }.to output.to_stdout
- end.to raise_error(SystemExit)
+ it 'parses -h' do
+ expect do
+ expect { described_class.parse(%w[foo -h bar]) }.to output.to_stdout
+ end.to raise_error(SystemExit)
+ end
+
+ it 'assigns title' do
+ options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend])
+
+ expect(options.title).to eq 'foo bar baz'
+ end
end
- it 'assigns title' do
- options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend])
+ describe '.read_type' do
+ let(:type) { '1' }
- expect(options.title).to eq 'foo bar baz'
+ it 'reads type from $stdin' do
+ expect($stdin).to receive(:getc).and_return(type)
+ expect do
+ expect(described_class.read_type).to eq('added')
+ end.to output.to_stdout
+ end
+
+ context 'invalid type given' do
+ let(:type) { '99' }
+
+ it 'shows error message and exits the program' do
+ allow($stdin).to receive(:getc).and_return(type)
+ expect do
+ expect do
+ expect { described_class.read_type }.to raise_error(SystemExit)
+ end.to output("Invalid category index, please select an index between 1 and 7\n").to_stderr
+ end.to output.to_stdout
+ end
+ end
end
end
end
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index a31e44fa928..74634dac713 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -20,12 +20,12 @@ describe 'mail_room.yml' do
YAML.load(output)
end
- before(:each) do
+ before do
stub_env('GITLAB_REDIS_QUEUES_CONFIG_FILE', absolute_path(queues_config_path))
clear_queues_raw_config
end
- after(:each) do
+ after do
clear_queues_raw_config
end
diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb
index 65587064eb1..373260b3978 100644
--- a/spec/controllers/admin/projects_controller_spec.rb
+++ b/spec/controllers/admin/projects_controller_spec.rb
@@ -12,12 +12,24 @@ describe Admin::ProjectsController do
it 'retrieves the project for the given visibility level' do
get :index, visibility_level: [Gitlab::VisibilityLevel::PUBLIC]
+
expect(response.body).to match(project.name)
end
it 'does not retrieve the project' do
get :index, visibility_level: [Gitlab::VisibilityLevel::INTERNAL]
+
expect(response.body).not_to match(project.name)
end
+
+ it 'does not respond with projects pending deletion' do
+ pending_delete_project = create(:project, pending_delete: true)
+
+ get :index
+
+ expect(response).to have_http_status(200)
+ expect(response.body).not_to match(pending_delete_project.name)
+ expect(response.body).to match(project.name)
+ end
end
end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 29c449d6aa9..aadd3317875 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -127,7 +127,7 @@ describe Admin::UsersController do
describe 'POST create' do
it 'creates the user' do
- expect{ post :create, user: attributes_for(:user) }.to change{ User.count }.by(1)
+ expect { post :create, user: attributes_for(:user) }.to change { User.count }.by(1)
end
it 'shows only one error message for an invalid email' do
@@ -150,6 +150,18 @@ describe Admin::UsersController do
post :update, params
end
+ context 'when the admin changes his own password' do
+ it 'updates the password' do
+ expect { update_password(admin, 'AValidPassword1') }
+ .to change { admin.reload.encrypted_password }
+ end
+
+ it 'does not set the new password to expire immediately' do
+ expect { update_password(admin, 'AValidPassword1') }
+ .not_to change { admin.reload.password_expires_at }
+ end
+ end
+
context 'when the new password is valid' do
it 'redirects to the user' do
update_password(user, 'AValidPassword1')
@@ -158,15 +170,13 @@ describe Admin::UsersController do
end
it 'updates the password' do
- update_password(user, 'AValidPassword1')
-
- expect { user.reload }.to change { user.encrypted_password }
+ expect { update_password(user, 'AValidPassword1') }
+ .to change { user.reload.encrypted_password }
end
it 'sets the new password to expire immediately' do
- update_password(user, 'AValidPassword1')
-
- expect { user.reload }.to change { user.password_expires_at }.to(a_value <= Time.now)
+ expect { update_password(user, 'AValidPassword1') }
+ .to change { user.reload.password_expires_at }.to be_within(2.seconds).of(Time.now)
end
end
@@ -184,9 +194,8 @@ describe Admin::UsersController do
end
it 'does not update the password' do
- update_password(user, 'invalid')
-
- expect { user.reload }.not_to change { user.encrypted_password }
+ expect { update_password(user, 'invalid') }
+ .not_to change { user.reload.encrypted_password }
end
end
@@ -204,9 +213,8 @@ describe Admin::UsersController do
end
it 'does not update the password' do
- update_password(user, 'AValidPassword1', 'AValidPassword2')
-
- expect { user.reload }.not_to change { user.encrypted_password }
+ expect { update_password(user, 'AValidPassword1', 'AValidPassword2') }
+ .not_to change { user.reload.encrypted_password }
end
end
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 331903a5543..59a6cfbf4f5 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -8,34 +8,43 @@ describe ApplicationController do
it 'redirects if the user is over their password expiry' do
user.password_expires_at = Time.new(2002)
+
expect(user.ldap_user?).to be_falsey
allow(controller).to receive(:current_user).and_return(user)
expect(controller).to receive(:redirect_to)
expect(controller).to receive(:new_profile_password_path)
+
controller.send(:check_password_expiration)
end
it 'does not redirect if the user is under their password expiry' do
user.password_expires_at = Time.now + 20010101
+
expect(user.ldap_user?).to be_falsey
allow(controller).to receive(:current_user).and_return(user)
expect(controller).not_to receive(:redirect_to)
+
controller.send(:check_password_expiration)
end
it 'does not redirect if the user is over their password expiry but they are an ldap user' do
user.password_expires_at = Time.new(2002)
+
allow(user).to receive(:ldap_user?).and_return(true)
allow(controller).to receive(:current_user).and_return(user)
expect(controller).not_to receive(:redirect_to)
+
controller.send(:check_password_expiration)
end
- it 'does not redirect if the user is over their password expiry but sign-in is disabled' do
+ it 'redirects if the user is over their password expiry and sign-in is disabled' do
stub_application_setting(password_authentication_enabled: false)
user.password_expires_at = Time.new(2002)
+
+ expect(user.ldap_user?).to be_falsey
allow(controller).to receive(:current_user).and_return(user)
- expect(controller).not_to receive(:redirect_to)
+ expect(controller).to receive(:redirect_to)
+ expect(controller).to receive(:new_profile_password_path)
controller.send(:check_password_expiration)
end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 3c396e36b24..be27bbb4283 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe AutocompleteController do
- let!(:project) { create(:project) }
- let!(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:user) { project.owner }
context 'GET users' do
let!(:user2) { create(:user) }
@@ -11,7 +11,6 @@ describe AutocompleteController do
context 'project members' do
before do
sign_in(user)
- project.add_master(user)
end
describe 'GET #users with project ID' do
@@ -19,11 +18,11 @@ describe AutocompleteController do
get(:users, project_id: project.id)
end
- let(:body) { JSON.parse(response.body) }
-
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 2 }
- it { expect(body.map { |u| u["username"] }).to include(user.username) }
+ it 'returns the project members' do
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.size).to eq(1)
+ expect(json_response.map { |u| u["username"] }).to include(user.username)
+ end
end
describe 'GET #users with unknown project' do
@@ -39,20 +38,20 @@ describe AutocompleteController do
let(:group) { create(:group) }
before do
- sign_in(user)
group.add_owner(user)
+ sign_in(user)
end
- let(:body) { JSON.parse(response.body) }
-
describe 'GET #users with group ID' do
before do
get(:users, group_id: group.id)
end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 1 }
- it { expect(body.first["username"]).to eq user.username }
+ it 'returns the group members' do
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.size).to eq(1)
+ expect(json_response.first["username"]).to eq user.username
+ end
end
describe 'GET #users with unknown group ID' do
@@ -65,23 +64,22 @@ describe AutocompleteController do
end
context 'non-member login for public project' do
- let!(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public) }
before do
sign_in(non_member)
- project.add_master(user)
end
- let(:body) { JSON.parse(response.body) }
-
describe 'GET #users with project ID' do
before do
get(:users, project_id: project.id, current_user: true)
end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 3 }
- it { expect(body.map { |u| u['username'] }).to include(user.username, non_member.username) }
+ it 'returns the project members and non-members' do
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |u| u['username'] }).to include(user.username, non_member.username)
+ end
end
end
@@ -91,10 +89,8 @@ describe AutocompleteController do
get(:users)
end
- let(:body) { JSON.parse(response.body) }
-
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq User.count }
+ it { expect(json_response).to be_kind_of(Array) }
+ it { expect(json_response.size).to eq User.count }
end
context 'user order' do
@@ -106,7 +102,7 @@ describe AutocompleteController do
sign_in(user)
get(:users, search: 'user')
- response_usernames = JSON.parse(response.body).map { |user| user['username'] }
+ response_usernames = json_response.map { |user| user['username'] }
expect(response_usernames.take(3)).to match_array([user.username, reported_user.username, user1.username])
end
@@ -120,15 +116,12 @@ describe AutocompleteController do
get(:users, per_page: per_page)
end
- let(:body) { JSON.parse(response.body) }
-
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq per_page }
+ it { expect(json_response).to be_kind_of(Array) }
+ it { expect(json_response.size).to eq(per_page) }
end
context 'unauthenticated user' do
let(:public_project) { create(:project, :public) }
- let(:body) { JSON.parse(response.body) }
describe 'GET #users with public project' do
before do
@@ -136,8 +129,8 @@ describe AutocompleteController do
get(:users, project_id: public_project.id)
end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 2 }
+ it { expect(json_response).to be_kind_of(Array) }
+ it { expect(json_response.size).to eq 2 }
end
describe 'GET #users with project' do
@@ -170,8 +163,8 @@ describe AutocompleteController do
get(:users)
end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 0 }
+ it { expect(json_response).to be_kind_of(Array) }
+ it { expect(json_response).to be_empty }
end
describe 'GET #users with todo filter' do
@@ -179,14 +172,12 @@ describe AutocompleteController do
get :users, todo_filter: true
expect(response.status).to eq 200
- expect(body).to be_kind_of(Array)
+ expect(json_response).to be_kind_of(Array)
end
end
end
context 'author of issuable included' do
- let(:body) { JSON.parse(response.body) }
-
context 'authenticated' do
before do
sign_in(user)
@@ -195,13 +186,13 @@ describe AutocompleteController do
it 'includes the author' do
get(:users, author_id: non_member.id)
- expect(body.first["username"]).to eq non_member.username
+ expect(json_response.first["username"]).to eq non_member.username
end
it 'rejects non existent user ids' do
get(:users, author_id: 99999)
- expect(body.collect { |u| u['id'] }).not_to include(99999)
+ expect(json_response.collect { |u| u['id'] }).not_to include(99999)
end
end
@@ -209,7 +200,7 @@ describe AutocompleteController do
it 'returns empty result' do
get(:users, author_id: non_member.id)
- expect(body).to be_empty
+ expect(json_response).to be_empty
end
end
end
@@ -222,10 +213,9 @@ describe AutocompleteController do
it 'skips the user IDs passed' do
get(:users, skip_users: [user, user2].map(&:id))
- other_user_ids = [non_member, project.owner, project.creator].map(&:id)
- response_user_ids = JSON.parse(response.body).map { |user| user['id'] }
+ response_user_ids = json_response.map { |user| user['id'] }
- expect(response_user_ids).to contain_exactly(*other_user_ids)
+ expect(response_user_ids).to contain_exactly(non_member.id)
end
end
end
@@ -249,17 +239,12 @@ describe AutocompleteController do
get(:projects, project_id: project.id)
end
- let(:body) { JSON.parse(response.body) }
-
- it do
- expect(body).to be_kind_of(Array)
- expect(body.size).to eq 2
+ it 'returns projects' do
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.size).to eq(1)
- expect(body.first['id']).to eq 0
- expect(body.first['name_with_namespace']).to eq 'No project'
-
- expect(body.last['id']).to eq authorized_project.id
- expect(body.last['name_with_namespace']).to eq authorized_project.name_with_namespace
+ expect(json_response.first['id']).to eq authorized_project.id
+ expect(json_response.first['name_with_namespace']).to eq authorized_project.name_with_namespace
end
end
end
@@ -275,14 +260,12 @@ describe AutocompleteController do
get(:projects, project_id: project.id, search: 'rugged')
end
- let(:body) { JSON.parse(response.body) }
-
- it do
- expect(body).to be_kind_of(Array)
- expect(body.size).to eq 2
+ it 'returns projects' do
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.size).to eq(1)
- expect(body.last['id']).to eq authorized_search_project.id
- expect(body.last['name_with_namespace']).to eq authorized_search_project.name_with_namespace
+ expect(json_response.first['id']).to eq authorized_search_project.id
+ expect(json_response.first['name_with_namespace']).to eq authorized_search_project.name_with_namespace
end
end
end
@@ -304,11 +287,9 @@ describe AutocompleteController do
get(:projects, project_id: project.id)
end
- let(:body) { JSON.parse(response.body) }
-
- it do
- expect(body).to be_kind_of(Array)
- expect(body.size).to eq 3 # Of a total of 4
+ it 'returns projects' do
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.size).to eq 2 # Of a total of 3
end
end
end
@@ -328,17 +309,15 @@ describe AutocompleteController do
get(:projects, project_id: project.id, offset_id: authorized_project.id)
end
- let(:body) { JSON.parse(response.body) }
-
- it do
- expect(body.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there
- expect(body.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either
+ it 'returns projects' do
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.size).to eq 2 # Of a total of 3
end
end
end
context 'authorized projects without admin_issue ability' do
- before(:each) do
+ before do
authorized_project.add_guest(user)
expect(user.can?(:admin_issue, authorized_project)).to eq(false)
@@ -349,14 +328,48 @@ describe AutocompleteController do
get(:projects, project_id: project.id)
end
- let(:body) { JSON.parse(response.body) }
+ it 'returns no projects' do
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.size).to eq(0)
+ end
+ end
+ end
+ end
+
+ context 'GET award_emojis' do
+ let(:user2) { create(:user) }
+ let!(:award_emoji1) { create_list(:award_emoji, 2, user: user, name: 'thumbsup') }
+ let!(:award_emoji2) { create_list(:award_emoji, 1, user: user, name: 'thumbsdown') }
+ let!(:award_emoji3) { create_list(:award_emoji, 3, user: user, name: 'star') }
+ let!(:award_emoji4) { create_list(:award_emoji, 1, user: user, name: 'tea') }
- it do
- expect(body).to be_kind_of(Array)
- expect(body.size).to eq 1 # 'No project'
+ context 'unauthorized user' do
+ it 'returns empty json' do
+ get :award_emojis
- expect(body.first['id']).to eq 0
- end
+ expect(json_response).to be_empty
+ end
+ end
+
+ context 'sign in as user without award emoji' do
+ it 'returns empty json' do
+ sign_in(user2)
+ get :award_emojis
+
+ expect(json_response).to be_empty
+ end
+ end
+
+ context 'sign in as user with award emoji' do
+ it 'returns json sorted by name count' do
+ sign_in(user)
+ get :award_emojis
+
+ expect(json_response.count).to eq 4
+ expect(json_response[0]).to match('name' => 'star')
+ expect(json_response[1]).to match('name' => 'thumbsup')
+ expect(json_response[2]).to match('name' => 'tea')
+ expect(json_response[3]).to match('name' => 'thumbsdown')
end
end
end
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
new file mode 100644
index 00000000000..dfa06c78d46
--- /dev/null
+++ b/spec/controllers/boards/issues_controller_spec.rb
@@ -0,0 +1,235 @@
+require 'spec_helper'
+
+describe Boards::IssuesController do
+ let(:project) { create(:project) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+ let(:guest) { create(:user) }
+
+ let(:planning) { create(:label, project: project, name: 'Planning') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+
+ let!(:list1) { create(:list, board: board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: board, label: development, position: 1) }
+
+ before do
+ project.team << [user, :master]
+ project.team << [guest, :guest]
+ end
+
+ describe 'GET index' do
+ let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
+
+ context 'with invalid board id' do
+ it 'returns a not found 404 response' do
+ list_issues user: user, board: 999, list: list2
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when list id is present' do
+ context 'with valid list id' do
+ it 'returns issues that have the list label applied' do
+ issue = create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
+ create(:labeled_issue, project: project, labels: [development], assignees: [johndoe])
+ issue.subscribe(johndoe, project)
+
+ list_issues user: user, board: board, list: list2
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('issues')
+ expect(parsed_response.length).to eq 2
+ expect(development.issues.map(&:relative_position)).not_to include(nil)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ list_issues user: user, board: board, list: 999
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when list id is missing' do
+ it 'returns opened issues without board labels applied' do
+ bug = create(:label, project: project, name: 'Bug')
+ create(:issue, project: project)
+ create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [development])
+ create(:labeled_issue, project: project, labels: [bug])
+
+ list_issues user: user, board: board
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('issues')
+ expect(parsed_response.length).to eq 2
+ end
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
+ end
+
+ it 'returns a forbidden 403 response' do
+ list_issues user: user, board: board, list: list2
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def list_issues(user:, board:, list: nil)
+ sign_in(user)
+
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ board_id: board.to_param,
+ list_id: list.try(:to_param)
+ }
+
+ get :index, params.compact
+ end
+ end
+
+ describe 'POST create' do
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ create_issue user: user, board: board, list: list1, title: 'New issue'
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the created issue' do
+ create_issue user: user, board: board, list: list1, title: 'New issue'
+
+ expect(response).to match_response_schema('issue')
+ end
+ end
+
+ context 'with invalid params' do
+ context 'when title is nil' do
+ it 'returns an unprocessable entity 422 response' do
+ create_issue user: user, board: board, list: list1, title: nil
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'when list does not belongs to project board' do
+ it 'returns a not found 404 response' do
+ list = create(:list)
+
+ create_issue user: user, board: board, list: list, title: 'New issue'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with invalid board id' do
+ it 'returns a not found 404 response' do
+ create_issue user: user, board: 999, list: list1, title: 'New issue'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ create_issue user: user, board: board, list: 999, title: 'New issue'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a forbidden 403 response' do
+ create_issue user: guest, board: board, list: list1, title: 'New issue'
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def create_issue(user:, board:, list:, title:)
+ sign_in(user)
+
+ post :create, board_id: board.to_param,
+ list_id: list.to_param,
+ issue: { title: title, project_id: project.id },
+ format: :json
+ end
+ end
+
+ describe 'PATCH update' do
+ let!(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
+
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ move user: user, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'moves issue to the desired list' do
+ move user: user, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(issue.reload.labels).to contain_exactly(development)
+ end
+ end
+
+ context 'with invalid params' do
+ it 'returns a unprocessable entity 422 response for invalid lists' do
+ move user: user, board: board, issue: issue, from_list_id: nil, to_list_id: nil
+
+ expect(response).to have_http_status(422)
+ end
+
+ it 'returns a not found 404 response for invalid board id' do
+ move user: user, board: 999, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a not found 404 response for invalid issue id' do
+ move user: user, board: board, issue: double(id: 999), from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:guest) { create(:user) }
+
+ before do
+ project.team << [guest, :guest]
+ end
+
+ it 'returns a forbidden 403 response' do
+ move user: guest, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def move(user:, board:, issue:, from_list_id:, to_list_id:)
+ sign_in(user)
+
+ patch :update, namespace_id: project.namespace.to_param,
+ project_id: project.id,
+ board_id: board.to_param,
+ id: issue.id,
+ from_list_id: from_list_id,
+ to_list_id: to_list_id,
+ format: :json
+ end
+ end
+end
diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb
new file mode 100644
index 00000000000..b11fce0fa58
--- /dev/null
+++ b/spec/controllers/boards/lists_controller_spec.rb
@@ -0,0 +1,252 @@
+require 'spec_helper'
+
+describe Boards::ListsController do
+ let(:project) { create(:project) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+ let(:guest) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ project.team << [guest, :guest]
+ end
+
+ describe 'GET index' do
+ it 'returns a successful 200 response' do
+ read_board_list user: user, board: board
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type).to eq 'application/json'
+ end
+
+ it 'returns a list of board lists' do
+ create(:list, board: board)
+
+ read_board_list user: user, board: board
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('lists')
+ expect(parsed_response.length).to eq 3
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability).to receive(:allowed?).with(user, :read_list, project).and_return(false)
+ end
+
+ it 'returns a forbidden 403 response' do
+ read_board_list user: user, board: board
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def read_board_list(user:, board:)
+ sign_in(user)
+
+ get :index, namespace_id: project.namespace.to_param,
+ project_id: project,
+ board_id: board.to_param,
+ format: :json
+ end
+ end
+
+ describe 'POST create' do
+ context 'with valid params' do
+ let(:label) { create(:label, project: project, name: 'Development') }
+
+ it 'returns a successful 200 response' do
+ create_board_list user: user, board: board, label_id: label.id
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the created list' do
+ create_board_list user: user, board: board, label_id: label.id
+
+ expect(response).to match_response_schema('list')
+ end
+ end
+
+ context 'with invalid params' do
+ context 'when label is nil' do
+ it 'returns a not found 404 response' do
+ create_board_list user: user, board: board, label_id: nil
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when label that does not belongs to project' do
+ it 'returns a not found 404 response' do
+ label = create(:label, name: 'Development')
+
+ create_board_list user: user, board: board, label_id: label.id
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a forbidden 403 response' do
+ label = create(:label, project: project, name: 'Development')
+
+ create_board_list user: guest, board: board, label_id: label.id
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def create_board_list(user:, board:, label_id:)
+ sign_in(user)
+
+ post :create, namespace_id: project.namespace.to_param,
+ project_id: project,
+ board_id: board.to_param,
+ list: { label_id: label_id },
+ format: :json
+ end
+ end
+
+ describe 'PATCH update' do
+ let!(:planning) { create(:list, board: board, position: 0) }
+ let!(:development) { create(:list, board: board, position: 1) }
+
+ context 'with valid position' do
+ it 'returns a successful 200 response' do
+ move user: user, board: board, list: planning, position: 1
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'moves the list to the desired position' do
+ move user: user, board: board, list: planning, position: 1
+
+ expect(planning.reload.position).to eq 1
+ end
+ end
+
+ context 'with invalid position' do
+ it 'returns an unprocessable entity 422 response' do
+ move user: user, board: board, list: planning, position: 6
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ move user: user, board: board, list: 999, position: 1
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a forbidden 403 response' do
+ move user: guest, board: board, list: planning, position: 6
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def move(user:, board:, list:, position:)
+ sign_in(user)
+
+ patch :update, namespace_id: project.namespace.to_param,
+ project_id: project,
+ board_id: board.to_param,
+ id: list.to_param,
+ list: { position: position },
+ format: :json
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let!(:planning) { create(:list, board: board, position: 0) }
+
+ context 'with valid list id' do
+ it 'returns a successful 200 response' do
+ remove_board_list user: user, board: board, list: planning
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'removes list from board' do
+ expect { remove_board_list user: user, board: board, list: planning }.to change(board.lists, :size).by(-1)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ remove_board_list user: user, board: board, list: 999
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a forbidden 403 response' do
+ remove_board_list user: guest, board: board, list: planning
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def remove_board_list(user:, board:, list:)
+ sign_in(user)
+
+ delete :destroy, namespace_id: project.namespace.to_param,
+ project_id: project,
+ board_id: board.to_param,
+ id: list.to_param,
+ format: :json
+ end
+ end
+
+ describe 'POST generate' do
+ context 'when board lists is empty' do
+ it 'returns a successful 200 response' do
+ generate_default_lists user: user, board: board
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the defaults lists' do
+ generate_default_lists user: user, board: board
+
+ expect(response).to match_response_schema('lists')
+ end
+ end
+
+ context 'when board lists is not empty' do
+ it 'returns an unprocessable entity 422 response' do
+ create(:list, board: board)
+
+ generate_default_lists user: user, board: board
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a forbidden 403 response' do
+ generate_default_lists user: guest, board: board
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def generate_default_lists(user:, board:)
+ sign_in(user)
+
+ post :generate, namespace_id: project.namespace.to_param,
+ project_id: project,
+ board_id: board.to_param,
+ format: :json
+ end
+ end
+end
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
new file mode 100644
index 00000000000..c9687af4dd2
--- /dev/null
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe IssuableCollections do
+ let(:user) { create(:user) }
+
+ let(:controller) do
+ klass = Class.new do
+ def self.helper_method(name); end
+
+ include IssuableCollections
+ end
+
+ controller = klass.new
+
+ allow(controller).to receive(:params).and_return(state: 'opened')
+
+ controller
+ end
+
+ describe '#redirect_out_of_range' do
+ before do
+ allow(controller).to receive(:url_for)
+ end
+
+ it 'returns true and redirects if the offset is out of range' do
+ relation = double(:relation, current_page: 10)
+
+ expect(controller).to receive(:redirect_to)
+ expect(controller.send(:redirect_out_of_range, relation, 2)).to eq(true)
+ end
+
+ it 'returns false if the offset is not out of range' do
+ relation = double(:relation, current_page: 1)
+
+ expect(controller).not_to receive(:redirect_to)
+ expect(controller.send(:redirect_out_of_range, relation, 2)).to eq(false)
+ end
+ end
+
+ describe '#issues_page_count' do
+ it 'returns the number of issue pages' do
+ project = create(:project, :public)
+
+ create(:issue, project: project)
+
+ finder = IssuesFinder.new(user)
+ issues = finder.execute
+
+ allow(controller).to receive(:issues_finder)
+ .and_return(finder)
+
+ expect(controller.send(:issues_page_count, issues)).to eq(1)
+ end
+ end
+
+ describe '#merge_requests_page_count' do
+ it 'returns the number of merge request pages' do
+ project = create(:project, :public)
+
+ create(:merge_request, source_project: project, target_project: project)
+
+ finder = MergeRequestsFinder.new(user)
+ merge_requests = finder.execute
+
+ allow(controller).to receive(:merge_requests_finder)
+ .and_return(finder)
+
+ pages = controller.send(:merge_requests_page_count, merge_requests)
+
+ expect(pages).to eq(1)
+ end
+ end
+
+ describe '#page_count_for_relation' do
+ it 'returns the number of pages' do
+ relation = double(:relation, limit_value: 20)
+ pages = controller.send(:page_count_for_relation, relation, 28)
+
+ expect(pages).to eq(2)
+ end
+ end
+end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index c2ada8c8df7..b0564e27a68 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -2,9 +2,133 @@ require 'rails_helper'
describe GroupsController do
let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, namespace: group) }
let!(:group_member) { create(:group_member, group: group, user: user) }
+ let!(:owner) { group.add_owner(create(:user)).user }
+ let!(:master) { group.add_master(create(:user)).user }
+ let!(:developer) { group.add_developer(create(:user)).user }
+ let!(:guest) { group.add_guest(create(:user)).user }
+
+ shared_examples 'member with ability to create subgroups' do
+ it 'renders the new page' do
+ sign_in(member)
+
+ get :new, parent_id: group.id
+
+ expect(response).to render_template(:new)
+ end
+ end
+
+ shared_examples 'member without ability to create subgroups' do
+ it 'renders the 404 page' do
+ sign_in(member)
+
+ get :new, parent_id: group.id
+
+ expect(response).not_to render_template(:new)
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe 'GET #new' do
+ context 'when creating subgroups', :nested_groups do
+ [true, false].each do |can_create_group_status|
+ context "and can_create_group is #{can_create_group_status}" do
+ before do
+ User.where(id: [admin, owner, master, developer, guest]).update_all(can_create_group: can_create_group_status)
+ end
+
+ [:admin, :owner].each do |member_type|
+ context "and logged in as #{member_type.capitalize}" do
+ it_behaves_like 'member with ability to create subgroups' do
+ let(:member) { send(member_type) }
+ end
+ end
+ end
+
+ [:guest, :developer, :master].each do |member_type|
+ context "and logged in as #{member_type.capitalize}" do
+ it_behaves_like 'member without ability to create subgroups' do
+ let(:member) { send(member_type) }
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe 'POST #create' do
+ context 'when creating subgroups', :nested_groups do
+ [true, false].each do |can_create_group_status|
+ context "and can_create_group is #{can_create_group_status}" do
+ context 'and logged in as Owner' do
+ it 'creates the subgroup' do
+ owner.update_attribute(:can_create_group, can_create_group_status)
+ sign_in(owner)
+
+ post :create, group: { parent_id: group.id, path: 'subgroup' }
+
+ expect(response).to be_redirect
+ expect(response.body).to match(%r{http://test.host/#{group.path}/subgroup})
+ end
+ end
+
+ context 'and logged in as Developer' do
+ it 'renders the new template' do
+ developer.update_attribute(:can_create_group, can_create_group_status)
+ sign_in(developer)
+
+ previous_group_count = Group.count
+
+ post :create, group: { parent_id: group.id, path: 'subgroup' }
+
+ expect(response).to render_template(:new)
+ expect(Group.count).to eq(previous_group_count)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when creating a top level group' do
+ before do
+ sign_in(developer)
+ end
+
+ context 'and can_create_group is enabled' do
+ before do
+ developer.update_attribute(:can_create_group, true)
+ end
+
+ it 'creates the Group' do
+ original_group_count = Group.count
+
+ post :create, group: { path: 'subgroup' }
+
+ expect(Group.count).to eq(original_group_count + 1)
+ expect(response).to be_redirect
+ end
+ end
+
+ context 'and can_create_group is disabled' do
+ before do
+ developer.update_attribute(:can_create_group, false)
+ end
+
+ it 'does not create the Group' do
+ original_group_count = Group.count
+
+ post :create, group: { path: 'subgroup' }
+
+ expect(Group.count).to eq(original_group_count)
+ expect(response).to render_template(:new)
+ end
+ end
+ end
+ end
describe 'GET #index' do
context 'as a user' do
diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb
index e478a253b3f..e00403118a0 100644
--- a/spec/controllers/invites_controller_spec.rb
+++ b/spec/controllers/invites_controller_spec.rb
@@ -24,7 +24,7 @@ describe InvitesController do
describe 'GET #decline' do
it 'declines user' do
get :decline, id: token
- expect{member.reload}.to raise_error ActiveRecord::RecordNotFound
+ expect {member.reload}.to raise_error ActiveRecord::RecordNotFound
expect(response).to have_http_status(302)
expect(flash[:notice]).to include 'You have declined the invitation to join'
diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb
index 2955d01fad0..cdaa88bbf5d 100644
--- a/spec/controllers/passwords_controller_spec.rb
+++ b/spec/controllers/passwords_controller_spec.rb
@@ -1,18 +1,18 @@
require 'spec_helper'
describe PasswordsController do
- describe '#check_password_authentication_available' do
+ describe '#prevent_ldap_reset' do
before do
@request.env["devise.mapping"] = Devise.mappings[:user]
end
context 'when password authentication is disabled' do
- it 'prevents a password reset' do
+ it 'allows password reset' do
stub_application_setting(password_authentication_enabled: false)
post :create
- expect(flash[:alert]).to eq 'Password authentication is unavailable.'
+ expect(response).to have_http_status(302)
end
end
@@ -22,7 +22,7 @@ describe PasswordsController do
it 'prevents a password reset' do
post :create, user: { email: user.email }
- expect(flash[:alert]).to eq 'Password authentication is unavailable.'
+ expect(flash[:alert]).to eq('Cannot reset password for LDAP user.')
end
end
end
diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb
index a5f544b4f92..a66b4ab0902 100644
--- a/spec/controllers/profiles/preferences_controller_spec.rb
+++ b/spec/controllers/profiles/preferences_controller_spec.rb
@@ -25,7 +25,8 @@ describe Profiles::PreferencesController do
def go(params: {}, format: :js)
params.reverse_merge!(
color_scheme_id: '1',
- dashboard: 'stars'
+ dashboard: 'stars',
+ theme_id: '1'
)
patch :update, user: params, format: format
@@ -40,7 +41,8 @@ describe Profiles::PreferencesController do
it "changes the user's preferences" do
prefs = {
color_scheme_id: '1',
- dashboard: 'stars'
+ dashboard: 'stars',
+ theme_id: '2'
}.with_indifferent_access
expect(user).to receive(:assign_attributes).with(prefs)
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 9d60dab12d1..b52b63e05a4 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -16,7 +16,11 @@ describe ProfilesController do
end
it "ignores an email update from a user with an external email address" do
- ldap_user = create(:omniauth_user, external_email: true)
+ stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
+ stub_omniauth_setting(sync_profile_attributes: true)
+
+ ldap_user = create(:omniauth_user)
+ ldap_user.create_user_synced_attributes_metadata(provider: 'ldap', name_synced: true, email_synced: true)
sign_in(ldap_user)
put :update,
@@ -27,5 +31,24 @@ describe ProfilesController do
expect(response.status).to eq(302)
expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com')
end
+
+ it "ignores an email and name update but allows a location update from a user with external email and name, but not external location" do
+ stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
+ stub_omniauth_setting(sync_profile_attributes: true)
+
+ ldap_user = create(:omniauth_user, name: 'Alex')
+ ldap_user.create_user_synced_attributes_metadata(provider: 'ldap', name_synced: true, email_synced: true, location_synced: false)
+ sign_in(ldap_user)
+
+ put :update,
+ user: { email: "john@gmail.com", name: "John", location: "City, Country" }
+
+ ldap_user.reload
+
+ expect(response.status).to eq(302)
+ expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com')
+ expect(ldap_user.name).not_to eq('John')
+ expect(ldap_user.location).to eq('City, Country')
+ end
end
end
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index d2c613a2423..caa63e7bd22 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -81,14 +81,6 @@ describe Projects::ArtifactsController do
expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt'))
end
end
-
- context 'when the file does not exist' do
- it 'responds Not Found' do
- get :raw, namespace_id: project.namespace, project_id: project, job_id: job, path: 'unknown'
-
- expect(response).to be_not_found
- end
- end
end
describe 'GET latest_succeeded' do
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
deleted file mode 100644
index 3f6c1092163..00000000000
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ /dev/null
@@ -1,221 +0,0 @@
-require 'spec_helper'
-
-describe Projects::Boards::IssuesController do
- let(:project) { create(:project) }
- let(:board) { create(:board, project: project) }
- let(:user) { create(:user) }
- let(:guest) { create(:user) }
-
- let(:planning) { create(:label, project: project, name: 'Planning') }
- let(:development) { create(:label, project: project, name: 'Development') }
-
- let!(:list1) { create(:list, board: board, label: planning, position: 0) }
- let!(:list2) { create(:list, board: board, label: development, position: 1) }
-
- before do
- project.team << [user, :master]
- project.team << [guest, :guest]
- end
-
- describe 'GET index' do
- let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
-
- context 'with invalid board id' do
- it 'returns a not found 404 response' do
- list_issues user: user, board: 999, list: list2
-
- expect(response).to have_http_status(404)
- end
- end
-
- context 'when list id is present' do
- context 'with valid list id' do
- it 'returns issues that have the list label applied' do
- issue = create(:labeled_issue, project: project, labels: [planning])
- create(:labeled_issue, project: project, labels: [planning])
- create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
- create(:labeled_issue, project: project, labels: [development], assignees: [johndoe])
- issue.subscribe(johndoe, project)
-
- list_issues user: user, board: board, list: list2
-
- parsed_response = JSON.parse(response.body)
-
- expect(response).to match_response_schema('issues')
- expect(parsed_response.length).to eq 2
- expect(development.issues.map(&:relative_position)).not_to include(nil)
- end
- end
-
- context 'with invalid list id' do
- it 'returns a not found 404 response' do
- list_issues user: user, board: board, list: 999
-
- expect(response).to have_http_status(404)
- end
- end
- end
-
- context 'when list id is missing' do
- it 'returns opened issues without board labels applied' do
- bug = create(:label, project: project, name: 'Bug')
- create(:issue, project: project)
- create(:labeled_issue, project: project, labels: [planning])
- create(:labeled_issue, project: project, labels: [development])
- create(:labeled_issue, project: project, labels: [bug])
-
- list_issues user: user, board: board
-
- parsed_response = JSON.parse(response.body)
-
- expect(response).to match_response_schema('issues')
- expect(parsed_response.length).to eq 2
- end
- end
-
- context 'with unauthorized user' do
- before do
- allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
- allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
- end
-
- it 'returns a forbidden 403 response' do
- list_issues user: user, board: board, list: list2
-
- expect(response).to have_http_status(403)
- end
- end
-
- def list_issues(user:, board:, list: nil)
- sign_in(user)
-
- params = {
- namespace_id: project.namespace.to_param,
- project_id: project,
- board_id: board.to_param,
- list_id: list.try(:to_param)
- }
-
- get :index, params.compact
- end
- end
-
- describe 'POST create' do
- context 'with valid params' do
- it 'returns a successful 200 response' do
- create_issue user: user, board: board, list: list1, title: 'New issue'
-
- expect(response).to have_http_status(200)
- end
-
- it 'returns the created issue' do
- create_issue user: user, board: board, list: list1, title: 'New issue'
-
- expect(response).to match_response_schema('issue')
- end
- end
-
- context 'with invalid params' do
- context 'when title is nil' do
- it 'returns an unprocessable entity 422 response' do
- create_issue user: user, board: board, list: list1, title: nil
-
- expect(response).to have_http_status(422)
- end
- end
-
- context 'when list does not belongs to project board' do
- it 'returns a not found 404 response' do
- list = create(:list)
-
- create_issue user: user, board: board, list: list, title: 'New issue'
-
- expect(response).to have_http_status(404)
- end
- end
- end
-
- context 'with unauthorized user' do
- it 'returns a forbidden 403 response' do
- create_issue user: guest, board: board, list: list1, title: 'New issue'
-
- expect(response).to have_http_status(403)
- end
- end
-
- def create_issue(user:, board:, list:, title:)
- sign_in(user)
-
- post :create, namespace_id: project.namespace.to_param,
- project_id: project,
- board_id: board.to_param,
- list_id: list.to_param,
- issue: { title: title },
- format: :json
- end
- end
-
- describe 'PATCH update' do
- let(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
-
- context 'with valid params' do
- it 'returns a successful 200 response' do
- move user: user, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
-
- expect(response).to have_http_status(200)
- end
-
- it 'moves issue to the desired list' do
- move user: user, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
-
- expect(issue.reload.labels).to contain_exactly(development)
- end
- end
-
- context 'with invalid params' do
- it 'returns a unprocessable entity 422 response for invalid lists' do
- move user: user, board: board, issue: issue, from_list_id: nil, to_list_id: nil
-
- expect(response).to have_http_status(422)
- end
-
- it 'returns a not found 404 response for invalid board id' do
- move user: user, board: 999, issue: issue, from_list_id: list1.id, to_list_id: list2.id
-
- expect(response).to have_http_status(404)
- end
-
- it 'returns a not found 404 response for invalid issue id' do
- move user: user, board: board, issue: 999, from_list_id: list1.id, to_list_id: list2.id
-
- expect(response).to have_http_status(404)
- end
- end
-
- context 'with unauthorized user' do
- let(:guest) { create(:user) }
-
- before do
- project.team << [guest, :guest]
- end
-
- it 'returns a forbidden 403 response' do
- move user: guest, board: board, issue: issue, from_list_id: list1.id, to_list_id: list2.id
-
- expect(response).to have_http_status(403)
- end
- end
-
- def move(user:, board:, issue:, from_list_id:, to_list_id:)
- sign_in(user)
-
- patch :update, namespace_id: project.namespace.to_param,
- project_id: project,
- board_id: board.to_param,
- id: issue.to_param,
- from_list_id: from_list_id,
- to_list_id: to_list_id,
- format: :json
- end
- end
-end
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
deleted file mode 100644
index 65beec16307..00000000000
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ /dev/null
@@ -1,252 +0,0 @@
-require 'spec_helper'
-
-describe Projects::Boards::ListsController do
- let(:project) { create(:project) }
- let(:board) { create(:board, project: project) }
- let(:user) { create(:user) }
- let(:guest) { create(:user) }
-
- before do
- project.team << [user, :master]
- project.team << [guest, :guest]
- end
-
- describe 'GET index' do
- it 'returns a successful 200 response' do
- read_board_list user: user, board: board
-
- expect(response).to have_http_status(200)
- expect(response.content_type).to eq 'application/json'
- end
-
- it 'returns a list of board lists' do
- create(:list, board: board)
-
- read_board_list user: user, board: board
-
- parsed_response = JSON.parse(response.body)
-
- expect(response).to match_response_schema('lists')
- expect(parsed_response.length).to eq 3
- end
-
- context 'with unauthorized user' do
- before do
- allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
- allow(Ability).to receive(:allowed?).with(user, :read_list, project).and_return(false)
- end
-
- it 'returns a forbidden 403 response' do
- read_board_list user: user, board: board
-
- expect(response).to have_http_status(403)
- end
- end
-
- def read_board_list(user:, board:)
- sign_in(user)
-
- get :index, namespace_id: project.namespace.to_param,
- project_id: project,
- board_id: board.to_param,
- format: :json
- end
- end
-
- describe 'POST create' do
- context 'with valid params' do
- let(:label) { create(:label, project: project, name: 'Development') }
-
- it 'returns a successful 200 response' do
- create_board_list user: user, board: board, label_id: label.id
-
- expect(response).to have_http_status(200)
- end
-
- it 'returns the created list' do
- create_board_list user: user, board: board, label_id: label.id
-
- expect(response).to match_response_schema('list')
- end
- end
-
- context 'with invalid params' do
- context 'when label is nil' do
- it 'returns a not found 404 response' do
- create_board_list user: user, board: board, label_id: nil
-
- expect(response).to have_http_status(404)
- end
- end
-
- context 'when label that does not belongs to project' do
- it 'returns a not found 404 response' do
- label = create(:label, name: 'Development')
-
- create_board_list user: user, board: board, label_id: label.id
-
- expect(response).to have_http_status(404)
- end
- end
- end
-
- context 'with unauthorized user' do
- it 'returns a forbidden 403 response' do
- label = create(:label, project: project, name: 'Development')
-
- create_board_list user: guest, board: board, label_id: label.id
-
- expect(response).to have_http_status(403)
- end
- end
-
- def create_board_list(user:, board:, label_id:)
- sign_in(user)
-
- post :create, namespace_id: project.namespace.to_param,
- project_id: project,
- board_id: board.to_param,
- list: { label_id: label_id },
- format: :json
- end
- end
-
- describe 'PATCH update' do
- let!(:planning) { create(:list, board: board, position: 0) }
- let!(:development) { create(:list, board: board, position: 1) }
-
- context 'with valid position' do
- it 'returns a successful 200 response' do
- move user: user, board: board, list: planning, position: 1
-
- expect(response).to have_http_status(200)
- end
-
- it 'moves the list to the desired position' do
- move user: user, board: board, list: planning, position: 1
-
- expect(planning.reload.position).to eq 1
- end
- end
-
- context 'with invalid position' do
- it 'returns an unprocessable entity 422 response' do
- move user: user, board: board, list: planning, position: 6
-
- expect(response).to have_http_status(422)
- end
- end
-
- context 'with invalid list id' do
- it 'returns a not found 404 response' do
- move user: user, board: board, list: 999, position: 1
-
- expect(response).to have_http_status(404)
- end
- end
-
- context 'with unauthorized user' do
- it 'returns a forbidden 403 response' do
- move user: guest, board: board, list: planning, position: 6
-
- expect(response).to have_http_status(403)
- end
- end
-
- def move(user:, board:, list:, position:)
- sign_in(user)
-
- patch :update, namespace_id: project.namespace.to_param,
- project_id: project,
- board_id: board.to_param,
- id: list.to_param,
- list: { position: position },
- format: :json
- end
- end
-
- describe 'DELETE destroy' do
- let!(:planning) { create(:list, board: board, position: 0) }
-
- context 'with valid list id' do
- it 'returns a successful 200 response' do
- remove_board_list user: user, board: board, list: planning
-
- expect(response).to have_http_status(200)
- end
-
- it 'removes list from board' do
- expect { remove_board_list user: user, board: board, list: planning }.to change(board.lists, :size).by(-1)
- end
- end
-
- context 'with invalid list id' do
- it 'returns a not found 404 response' do
- remove_board_list user: user, board: board, list: 999
-
- expect(response).to have_http_status(404)
- end
- end
-
- context 'with unauthorized user' do
- it 'returns a forbidden 403 response' do
- remove_board_list user: guest, board: board, list: planning
-
- expect(response).to have_http_status(403)
- end
- end
-
- def remove_board_list(user:, board:, list:)
- sign_in(user)
-
- delete :destroy, namespace_id: project.namespace.to_param,
- project_id: project,
- board_id: board.to_param,
- id: list.to_param,
- format: :json
- end
- end
-
- describe 'POST generate' do
- context 'when board lists is empty' do
- it 'returns a successful 200 response' do
- generate_default_lists user: user, board: board
-
- expect(response).to have_http_status(200)
- end
-
- it 'returns the defaults lists' do
- generate_default_lists user: user, board: board
-
- expect(response).to match_response_schema('lists')
- end
- end
-
- context 'when board lists is not empty' do
- it 'returns an unprocessable entity 422 response' do
- create(:list, board: board)
-
- generate_default_lists user: user, board: board
-
- expect(response).to have_http_status(422)
- end
- end
-
- context 'with unauthorized user' do
- it 'returns a forbidden 403 response' do
- generate_default_lists user: guest, board: board
-
- expect(response).to have_http_status(403)
- end
- end
-
- def generate_default_lists(user:, board:)
- sign_in(user)
-
- post :generate, namespace_id: project.namespace.to_param,
- project_id: project,
- board_id: board.to_param,
- format: :json
- end
- end
-end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index bdee3894a13..5d9403c23ac 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1,7 +1,7 @@
require('spec_helper')
describe Projects::IssuesController do
- let(:project) { create(:project_empty_repo) }
+ let(:project) { create(:project) }
let(:user) { create(:user) }
let(:issue) { create(:issue, project: project) }
@@ -233,144 +233,119 @@ describe Projects::IssuesController do
end
end
- context 'when moving issue to another private project' do
- let(:another_project) { create(:project, :private) }
-
- context 'when user has access to move issue' do
- before do
- another_project.team << [user, :reporter]
- end
-
- it 'moves issue to another project' do
- move_issue
+ context 'Akismet is enabled' do
+ let(:project) { create(:project_empty_repo, :public) }
- expect(response).to have_http_status :found
- expect(another_project.issues).not_to be_empty
- end
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
end
- context 'when user does not have access to move issue' do
- it 'responds with 404' do
- move_issue
+ 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
- expect(response).to have_http_status :not_found
+ it 'normally updates the issue' do
+ expect { update_issue(title: 'Foo') }.to change { issue.reload.title }.to('Foo')
end
end
- context 'Akismet is enabled' do
- let(:project) { create(:project_empty_repo, :public) }
-
+ context 'when an issue is identified as spam' do
before do
- stub_application_setting(recaptcha_enabled: true)
- allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(: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(:is_spam?).and_return(false)
+ context 'when captcha is not verified' do
+ def update_spam_issue
+ update_issue(title: 'Spam Title', description: 'Spam lives here')
end
- it 'normally updates the issue' do
- expect { update_issue(title: 'Foo') }.to change { issue.reload.title }.to('Foo')
+ before do
+ allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
end
- end
- context 'when an issue is identified as spam' do
- before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ 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
- context 'when captcha is not verified' do
- def update_spam_issue
- update_issue(title: 'Spam Title', description: 'Spam lives here')
- end
+ it 'rejects an issue recognized as a spam when recaptcha disabled' do
+ stub_application_setting(recaptcha_enabled: false)
- before do
- allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
- end
+ expect { update_spam_issue }.not_to change { issue.reload.title }
+ 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 'creates a spam log' do
+ update_spam_issue
- it 'rejects an issue recognized as a spam when recaptcha disabled' do
- stub_application_setting(recaptcha_enabled: false)
+ spam_logs = SpamLog.all
- expect { update_spam_issue }.not_to change{ issue.reload.title }
- end
+ 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 'creates a spam log' do
+ context 'as HTML' do
+ it 'renders verify template' 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
+ expect(response).to render_template(:verify)
end
+ end
- context 'as HTML' do
- it 'renders verify template' do
- update_spam_issue
-
- expect(response).to render_template(:verify)
- end
+ context 'as JSON' do
+ before do
+ update_issue({ title: 'Spam Title', description: 'Spam lives here' }, format: :json)
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 '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
+ 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) }
+ 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
+ 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
+ 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
+ it 'redirect to issue page' do
+ update_verified_issue
- expect(response)
- .to redirect_to(project_issue_path(project, issue))
- end
+ 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 '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 '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)
+ 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
+ expect { update_issue(spam_log_id: spam_log.id, recaptcha_verification: true) }
+ .not_to change { SpamLog.last.recaptcha_verified }
end
end
end
@@ -385,13 +360,45 @@ describe Projects::IssuesController do
put :update, params
end
+ end
+ end
+
+ describe 'POST #move' do
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ end
+
+ context 'when moving issue to another private project' do
+ let(:another_project) { create(:project, :private) }
+
+ context 'when user has access to move issue' do
+ before do
+ another_project.add_reporter(user)
+ end
+
+ it 'moves issue to another project' do
+ move_issue
+
+ expect(response).to have_http_status :ok
+ expect(another_project.issues).not_to be_empty
+ end
+ end
+
+ context 'when user does not have access to move issue' do
+ it 'responds with 404' do
+ move_issue
+
+ expect(response).to have_http_status :not_found
+ end
+ end
def move_issue
- put :update,
+ post :move,
+ format: :json,
namespace_id: project.namespace.to_param,
project_id: project,
id: issue.iid,
- issue: { title: 'New title' },
move_to_project_id: another_project.id
end
end
@@ -672,7 +679,7 @@ describe Projects::IssuesController do
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(:is_spam?).and_return(false)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false)
end
it 'does not create an issue' do
@@ -682,7 +689,7 @@ describe Projects::IssuesController do
context 'when an issue is identified as spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
context 'when captcha is not verified' do
@@ -841,7 +848,7 @@ describe Projects::IssuesController do
describe 'POST #toggle_award_emoji' do
before do
sign_in(user)
- project.team << [user, :developer]
+ project.add_developer(user)
end
it "toggles the award emoji" do
@@ -855,6 +862,8 @@ describe Projects::IssuesController do
end
describe 'POST create_merge_request' do
+ let(:project) { create(:project, :repository) }
+
before do
project.add_developer(user)
sign_in(user)
@@ -877,4 +886,19 @@ describe Projects::IssuesController do
format: :json
end
end
+
+ describe 'GET #discussions' do
+ let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'returns discussion json' do
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+
+ expect(JSON.parse(response.body).first.keys).to match_array(%w[id reply_id expanded notes individual_note])
+ end
+ end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index bb67db268fa..6775012bab5 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -56,6 +56,28 @@ describe Projects::MergeRequestsController do
expect(response).to be_success
end
+
+ context "loads notes" do
+ let(:first_contributor) { create(:user) }
+ let(:contributor) { create(:user) }
+ let(:merge_request) { create(:merge_request, author: first_contributor, target_project: project, source_project: project) }
+ let(:contributor_merge_request) { create(:merge_request, :merged, author: contributor, target_project: project, source_project: project) }
+ # the order here is important
+ # as the controller reloads these from DB, references doesn't correspond after
+ let!(:first_contributor_note) { create(:note, author: first_contributor, noteable: merge_request, project: project) }
+ let!(:contributor_note) { create(:note, author: contributor, noteable: merge_request, project: project) }
+ let!(:owner_note) { create(:note, author: user, noteable: merge_request, project: project) }
+
+ it "with special_role FIRST_TIME_CONTRIBUTOR" do
+ go(format: :html)
+
+ notes = assigns(:notes)
+ expect(notes).to match(a_collection_containing_exactly(an_object_having_attributes(special_role: Note::SpecialRole::FIRST_TIME_CONTRIBUTOR),
+ an_object_having_attributes(special_role: nil),
+ an_object_having_attributes(special_role: nil)
+ ))
+ end
+ end
end
describe 'as json' do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index f280c55059c..6ffe41b8608 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -46,10 +46,13 @@ describe Projects::NotesController do
end
context 'for a discussion note' do
- let!(:note) { create(:discussion_note_on_issue, noteable: issue, project: project) }
+ let(:project) { create(:project, :repository) }
+ let!(:note) { create(:discussion_note_on_merge_request, project: project) }
+
+ let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) }
it 'responds with the expected attributes' do
- get :index, request_params
+ get :index, params
expect(note_json[:id]).to eq(note.id)
expect(note_json[:discussion_html]).not_to be_nil
@@ -104,10 +107,12 @@ describe Projects::NotesController do
end
context 'for a regular note' do
- let!(:note) { create(:note, noteable: issue, project: project) }
+ let!(:note) { create(:note_on_merge_request, project: project) }
+
+ let(:params) { request_params.merge(target_type: 'merge_request', target_id: note.noteable_id) }
it 'responds with the expected attributes' do
- get :index, request_params
+ get :index, params
expect(note_json[:id]).to eq(note.id)
expect(note_json[:html]).not_to be_nil
@@ -125,7 +130,9 @@ describe Projects::NotesController do
note: { note: 'some note', noteable_id: merge_request.id, noteable_type: 'MergeRequest' },
namespace_id: project.namespace,
project_id: project,
- merge_request_diff_head_sha: 'sha'
+ merge_request_diff_head_sha: 'sha',
+ target_type: 'merge_request',
+ target_id: merge_request.id
}
end
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
index 4d0111302f3..83c7744a231 100644
--- a/spec/controllers/projects/pages_controller_spec.rb
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::PagesController do
let(:user) { create(:user) }
- let(:project) { create(:project, :public, :access_requestable) }
+ let(:project) { create(:project, :public) }
let(:request_params) do
{
@@ -23,6 +23,17 @@ describe Projects::PagesController do
expect(response).to have_http_status(200)
end
+
+ context 'when the project is in a subgroup' do
+ let(:group) { create(:group, :nested) }
+ let(:project) { create(:project, namespace: group) }
+
+ it 'returns a 404 status code' do
+ get :show, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
end
describe 'DELETE destroy' do
diff --git a/spec/controllers/projects/protected_tags_controller_spec.rb b/spec/controllers/projects/protected_tags_controller_spec.rb
index 64658988b3f..b6de90039f3 100644
--- a/spec/controllers/projects/protected_tags_controller_spec.rb
+++ b/spec/controllers/projects/protected_tags_controller_spec.rb
@@ -8,4 +8,21 @@ describe Projects::ProtectedTagsController do
get(:index, namespace_id: project.namespace.to_param, project_id: project)
end
end
+
+ describe "DELETE #destroy" do
+ let(:project) { create(:project, :repository) }
+ let(:protected_tag) { create(:protected_tag, :developers_can_create, project: project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it "deletes the protected tag" do
+ delete(:destroy, namespace_id: project.namespace.to_param, project_id: project, id: protected_tag.id)
+
+ expect { ProtectedTag.find(protected_tag.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 4e9b0c09ff2..efba9cc7306 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -10,9 +10,6 @@ describe Projects::ServicesController do
before do
sign_in(user)
project.team << [user, :master]
-
- controller.instance_variable_set(:@project, project)
- controller.instance_variable_set(:@service, service)
end
describe '#test' do
@@ -20,7 +17,7 @@ describe Projects::ServicesController do
it 'renders 404' do
allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
- put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id
+ put :test, namespace_id: project.namespace, project_id: project, id: service.to_param
expect(response).to have_http_status(404)
end
@@ -36,7 +33,7 @@ describe Projects::ServicesController do
it 'returns success' do
allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
- put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id
+ put :test, namespace_id: project.namespace, project_id: project, id: service.to_param
expect(response.status).to eq(200)
end
@@ -45,7 +42,7 @@ describe Projects::ServicesController do
it 'returns success' do
expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_return(hipchat_client)
- put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params
+ put :test, namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params
expect(response.status).to eq(200)
end
@@ -54,17 +51,42 @@ describe Projects::ServicesController do
it 'returns success' do
expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_return(hipchat_client)
- put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params
+ put :test, namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params
expect(response.status).to eq(200)
end
+
+ context 'when service is configured for the first time' do
+ before do
+ allow_any_instance_of(ServiceHook).to receive(:execute).and_return(true)
+ end
+
+ it 'persist the object' do
+ do_put
+
+ expect(BuildkiteService.first).to be_present
+ end
+
+ it 'creates the ServiceHook object' do
+ do_put
+
+ expect(BuildkiteService.first.service_hook).to be_present
+ end
+
+ def do_put
+ put :test, namespace_id: project.namespace,
+ project_id: project,
+ id: 'buildkite',
+ service: { 'active' => '1', 'push_events' => '1', token: 'token', 'project_url' => 'http://test.com' }
+ end
+ end
end
context 'failure' do
it 'returns success status code and the error message' do
expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_raise('Bad test')
- put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params
+ put :test, namespace_id: project.namespace, project_id: project, id: service.to_param, service: service_params
expect(response.status).to eq(200)
expect(JSON.parse(response.body))
@@ -77,7 +99,7 @@ describe Projects::ServicesController do
context 'when param `active` is set to true' do
it 'activates the service and redirects to integrations paths' do
put :update,
- namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: { active: true }
+ namespace_id: project.namespace, project_id: project, id: service.to_param, service: { active: true }
expect(response).to redirect_to(project_settings_integrations_path(project))
expect(flash[:notice]).to eq 'HipChat activated.'
@@ -87,7 +109,7 @@ describe Projects::ServicesController do
context 'when param `active` is set to false' do
it 'does not activate the service but saves the settings' do
put :update,
- namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: { active: false }
+ namespace_id: project.namespace, project_id: project, id: service.to_param, service: { active: false }
expect(flash[:notice]).to eq 'HipChat settings saved, but not activated.'
end
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index cc444f31797..3a1550aa730 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -98,7 +98,7 @@ describe Projects::SnippetsController do
context 'when the snippet is spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
context 'when the snippet is private' do
@@ -176,7 +176,7 @@ describe Projects::SnippetsController do
context 'when the snippet is spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
context 'when the snippet is private' do
diff --git a/spec/controllers/projects/todos_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb
index 974330e2bbd..41d211ed1bb 100644
--- a/spec/controllers/projects/todos_controller_spec.rb
+++ b/spec/controllers/projects/todos_controller_spec.rb
@@ -67,7 +67,7 @@ describe Projects::TodosController do
end
it "doesn't create todo" do
- expect{ go }.not_to change { user.todos.count }
+ expect { go }.not_to change { user.todos.count }
expect(response).to have_http_status(404)
end
end
@@ -135,7 +135,7 @@ describe Projects::TodosController do
end
it "doesn't create todo" do
- expect{ go }.not_to change { user.todos.count }
+ expect { go }.not_to change { user.todos.count }
expect(response).to have_http_status(404)
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 8ecd8b6ca71..4459e227fb3 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -7,6 +7,38 @@ describe ProjectsController do
let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+ describe 'GET new' do
+ context 'with an authenticated user' do
+ let(:group) { create(:group) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when namespace_id param is present' do
+ context 'when user has access to the namespace' do
+ it 'renders the template' do
+ group.add_owner(user)
+
+ get :new, namespace_id: group.id
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template('new')
+ end
+ end
+
+ context 'when user does not have access to the namespace' do
+ it 'responds with status 404' do
+ get :new, namespace_id: group.id
+
+ expect(response).to have_http_status(404)
+ expect(response).not_to render_template('new')
+ end
+ end
+ end
+ end
+ end
+
describe 'GET index' do
context 'as a user' do
it 'redirects to root page' do
@@ -578,6 +610,118 @@ describe ProjectsController do
end
end
+ describe '#export' do
+ before do
+ sign_in(user)
+
+ project.add_master(user)
+ end
+
+ context 'when project export is enabled' do
+ it 'returns 302' do
+ get :export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ context 'when project export is disabled' do
+ before do
+ stub_application_setting(project_export_enabled?: false)
+ end
+
+ it 'returns 404' do
+ get :export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe '#download_export' do
+ before do
+ sign_in(user)
+
+ project.add_master(user)
+ end
+
+ context 'when project export is enabled' do
+ it 'returns 302' do
+ get :download_export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ context 'when project export is disabled' do
+ before do
+ stub_application_setting(project_export_enabled?: false)
+ end
+
+ it 'returns 404' do
+ get :download_export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe '#remove_export' do
+ before do
+ sign_in(user)
+
+ project.add_master(user)
+ end
+
+ context 'when project export is enabled' do
+ it 'returns 302' do
+ post :remove_export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ context 'when project export is disabled' do
+ before do
+ stub_application_setting(project_export_enabled?: false)
+ end
+
+ it 'returns 404' do
+ post :remove_export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe '#generate_new_export' do
+ before do
+ sign_in(user)
+
+ project.add_master(user)
+ end
+
+ context 'when project export is enabled' do
+ it 'returns 302' do
+ post :generate_new_export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ context 'when project export is disabled' do
+ before do
+ stub_application_setting(project_export_enabled?: false)
+ end
+
+ it 'returns 404' do
+ post :generate_new_export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
def project_moved_message(redirect_route, project)
"Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path."
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 634563fc290..5a4ab39ab86 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -5,7 +5,7 @@ describe RegistrationsController do
let(:user_params) { { user: { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } }
context 'email confirmation' do
- around(:each) do |example|
+ around do |example|
perform_enqueued_jobs do
example.run
end
@@ -15,7 +15,7 @@ describe RegistrationsController do
it 'signs the user in' do
allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false)
- expect { post(:create, user_params) }.not_to change{ ActionMailer::Base.deliveries.size }
+ expect { post(:create, user_params) }.not_to change { ActionMailer::Base.deliveries.size }
expect(subject.current_user).not_to be_nil
end
end
diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb
index 1c494b8c7ab..225753333ee 100644
--- a/spec/controllers/snippets/notes_controller_spec.rb
+++ b/spec/controllers/snippets/notes_controller_spec.rb
@@ -138,7 +138,7 @@ describe Snippets::NotesController do
end
it "deletes the note" do
- expect{ delete :destroy, request_params }.to change{ Note.count }.from(1).to(0)
+ expect { delete :destroy, request_params }.to change { Note.count }.from(1).to(0)
end
context 'system note' do
@@ -147,7 +147,7 @@ describe Snippets::NotesController do
end
it "does not delete the note" do
- expect{ delete :destroy, request_params }.not_to change{ Note.count }
+ expect { delete :destroy, request_params }.not_to change { Note.count }
end
end
end
@@ -166,7 +166,7 @@ describe Snippets::NotesController do
end
it "does not update the note" do
- expect{ delete :destroy, request_params }.not_to change{ Note.count }
+ expect { delete :destroy, request_params }.not_to change { Note.count }
end
end
end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 475ceda11fe..be273acb69b 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -186,8 +186,8 @@ describe SnippetsController do
end
context 'when the snippet description contains a file' do
- let(:picture_file) { '/system/temp/secret56/picture.jpg' }
- let(:text_file) { '/system/temp/secret78/text.txt' }
+ let(:picture_file) { '/-/system/temp/secret56/picture.jpg' }
+ let(:text_file) { '/-/system/temp/secret78/text.txt' }
let(:description) do
"Description with picture: ![picture](/uploads#{picture_file}) and "\
"text: [text.txt](/uploads#{text_file})"
@@ -208,8 +208,8 @@ describe SnippetsController do
snippet = subject
expected_description = "Description with picture: "\
- "![picture](/uploads/system/personal_snippet/#{snippet.id}/secret56/picture.jpg) and "\
- "text: [text.txt](/uploads/system/personal_snippet/#{snippet.id}/secret78/text.txt)"
+ "![picture](/uploads/-/system/personal_snippet/#{snippet.id}/secret56/picture.jpg) and "\
+ "text: [text.txt](/uploads/-/system/personal_snippet/#{snippet.id}/secret78/text.txt)"
expect(snippet.description).to eq(expected_description)
end
@@ -217,7 +217,7 @@ describe SnippetsController do
context 'when the snippet is spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
context 'when the snippet is private' do
@@ -289,7 +289,7 @@ describe SnippetsController do
context 'when the snippet is spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
context 'when the snippet is private' do
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index b3a40f5d15c..b29f3d861be 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -102,7 +102,7 @@ describe UploadsController do
subject
expect(response.body).to match '\"alt\":\"rails_sample\"'
- expect(response.body).to match "\"url\":\"/uploads/system/temp"
+ expect(response.body).to match "\"url\":\"/uploads/-/system/temp"
end
it 'does not create an Upload record' do
@@ -119,7 +119,7 @@ describe UploadsController do
subject
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
- expect(response.body).to match "\"url\":\"/uploads/system/temp"
+ expect(response.body).to match "\"url\":\"/uploads/-/system/temp"
end
it 'does not create an Upload record' do
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index a64ad73cba8..2cecd2646fc 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -92,8 +92,14 @@ describe UsersController do
before do
sign_in(user)
project.team << [user, :developer]
- EventCreateService.new.push(project, user, [])
- EventCreateService.new.push(forked_project, user, [])
+
+ push_data = Gitlab::DataBuilder::Push.build_sample(project, user)
+
+ fork_push_data = Gitlab::DataBuilder::Push
+ .build_sample(forked_project, user)
+
+ EventCreateService.new.push(project, user, push_data)
+ EventCreateService.new.push(forked_project, user, fork_push_data)
end
it 'includes forked projects' do
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 97138fdf301..5d64c1b7161 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -12,6 +12,7 @@ FactoryGirl.define do
started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013'
commands 'ls -a'
+ protected false
options do
{
@@ -106,7 +107,7 @@ FactoryGirl.define do
end
trait :triggered do
- trigger_request factory: :ci_trigger_request_with_variables
+ trigger_request factory: :ci_trigger_request
end
after(:build) do |build, evaluator|
@@ -231,5 +232,9 @@ FactoryGirl.define do
status 'created'
self.when 'manual'
end
+
+ trait :protected do
+ protected true
+ end
end
end
diff --git a/spec/factories/ci/pipeline_variable_variables.rb b/spec/factories/ci/pipeline_variables.rb
index 7c1a7faec08..7c1a7faec08 100644
--- a/spec/factories/ci/pipeline_variable_variables.rb
+++ b/spec/factories/ci/pipeline_variables.rb
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index e83a0e599a8..e5ea6b41ea3 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -4,6 +4,7 @@ FactoryGirl.define do
ref 'master'
sha '97de212e80737a608d939f648d959671fb0a0142'
status 'pending'
+ protected false
project
@@ -59,6 +60,10 @@ FactoryGirl.define do
trait :failed do
status :failed
end
+
+ trait :protected do
+ protected true
+ end
end
end
end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index 05abf60d5ce..88bb755d068 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -5,6 +5,7 @@ FactoryGirl.define do
platform "darwin"
is_shared false
active true
+ access_level :not_protected
trait :online do
contacted_at Time.now
@@ -21,5 +22,9 @@ FactoryGirl.define do
trait :inactive do
active false
end
+
+ trait :ref_protected do
+ access_level :ref_protected
+ end
end
end
diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb
index d3c8bf9d54f..b2ded945738 100644
--- a/spec/factories/ci/stages.rb
+++ b/spec/factories/ci/stages.rb
@@ -15,4 +15,12 @@ FactoryGirl.define do
warnings: warnings)
end
end
+
+ factory :ci_stage_entity, class: Ci::Stage do
+ project factory: :project
+ pipeline factory: :ci_empty_pipeline
+
+ name 'test'
+ status 'pending'
+ end
end
diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb
index 10e0ab4fd3c..40b8848920e 100644
--- a/spec/factories/ci/trigger_requests.rb
+++ b/spec/factories/ci/trigger_requests.rb
@@ -1,14 +1,5 @@
FactoryGirl.define do
factory :ci_trigger_request, class: Ci::TriggerRequest do
trigger factory: :ci_trigger
-
- factory :ci_trigger_request_with_variables do
- variables do
- {
- TRIGGER_KEY_1: 'TRIGGER_VALUE_1',
- TRIGGER_KEY_2: 'TRIGGER_VALUE_2'
- }
- end
- end
end
end
diff --git a/spec/factories/ci/triggers.rb b/spec/factories/ci/triggers.rb
index 40c4663c6d8..3734c7040c0 100644
--- a/spec/factories/ci/triggers.rb
+++ b/spec/factories/ci/triggers.rb
@@ -1,5 +1,7 @@
FactoryGirl.define do
factory :ci_trigger_without_token, class: Ci::Trigger do
+ owner
+
factory :ci_trigger do
sequence(:token) { |n| "token#{n}" }
end
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 29ad1af9fd9..e5abfd67d60 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -10,6 +10,10 @@ FactoryGirl.define do
after(:build) do |deployment, evaluator|
deployment.project ||= deployment.environment.project
+
+ unless deployment.project.repository_exists?
+ allow(deployment.project.repository).to receive(:fetch_ref)
+ end
end
end
end
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index 11d2016955c..ad9f7e2caef 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -2,6 +2,7 @@ FactoryGirl.define do
factory :event do
project
author factory: :user
+ action Event::JOINED
trait(:created) { action Event::CREATED }
trait(:updated) { action Event::UPDATED }
@@ -20,4 +21,19 @@ FactoryGirl.define do
target factory: :closed_issue
end
end
+
+ factory :push_event, class: PushEvent do
+ project factory: :project_empty_repo
+ author factory: :user
+ action Event::PUSHED
+ end
+
+ factory :push_event_payload do
+ event
+ commit_count 1
+ action :pushed
+ ref_type :branch
+ ref 'master'
+ commit_to '3cdce97ed87c91368561584e7358f4d46e3e173c'
+ end
end
diff --git a/spec/factories/gpg_signature.rb b/spec/factories/gpg_signature.rb
index a5aeffbe12d..c0beecf0bea 100644
--- a/spec/factories/gpg_signature.rb
+++ b/spec/factories/gpg_signature.rb
@@ -6,6 +6,6 @@ FactoryGirl.define do
project
gpg_key
gpg_key_primary_keyid { gpg_key.primary_keyid }
- valid_signature true
+ verification_status :verified
end
end
diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb
index a13b6e3596e..3f7c794b14a 100644
--- a/spec/factories/keys.rb
+++ b/spec/factories/keys.rb
@@ -18,5 +18,54 @@ FactoryGirl.define do
factory :write_access_key, class: 'DeployKey' do
can_push true
end
+
+ factory :rsa_key_2048 do
+ key do
+ 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9' \
+ '6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5' \
+ '/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7' \
+ 'M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC' \
+ 'rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0' \
+ '5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com'
+ end
+
+ factory :rsa_deploy_key_2048, class: 'DeployKey'
+ end
+
+ factory :dsa_key_2048 do
+ key do
+ 'ssh-dss AAAAB3NzaC1kc3MAAAEBAO/3/NPLA/zSFkMOCaTtGo+uos1flfQ5f038Uk+G' \
+ 'Y9AeLGzX+Srhw59GdVXmOQLYBrOt5HdGwqYcmLnE2VurUGmhtfeO5H+3p5pGJbkS0Gxp' \
+ 'YH1HRO9lWsncF3Hh1w4lYsDjkclDiSTdfTuN8F4Kb3DXNnVSCieeonp+B25F/CXagyTQ' \
+ '/pvNmHFeYgGCVdnBtFdi+xfxaZ8NKdPrGggzokbKHElDZQ4Xo5EpdcyLajgM7nB2r2Rz' \
+ 'OrmeaevKi5lV68ehRa9Yyrb7vxvwiwBwOgqR/mnN7Gnaq1jUdmJY+ct04Qwx37f5jvhv' \
+ '5gA4U40SGMoiHM8RFIN7Ksz0jsyX73MAAAAVALRWOfjfzHpK7KLz4iqDvvTUAevJAAAB' \
+ 'AEa9NZ+6y9iQ5erGsdfLTXFrhSefTG0NhghoO/5IFkSGfd8V7kzTvCHaFrcfpEA5kP8t' \
+ 'poeOG0TASB6tgGOxm1Bq4Wncry5RORBPJlAVpDGRcvZ931ddH7IgltEInS6za2uH6F/1' \
+ 'M1QfKePSLr6xJ1ZLYfP0Og5KTp1x6yMQvfwV0a+XdA+EPgaJWLWp/pWwKWa0oLUgjsIH' \
+ 'MYzuOGh5c708uZrmkzqvgtW2NgXhcIroRgynT3IfI2lP2rqqb3uuuE/qH5UCUFO+Dc3H' \
+ 'nAFNeQDT/M25AERdPYBAY5a+iPjIgO+jT7BfmfByT+AZTqZySrCyc7nNZL3YgGLK0l6A' \
+ '1GgAAAEBAN9FpFOdIXE+YEZhKl1vPmbcn+b1y5zOl6N4x1B7Q8pD/pLMziWROIS8uLzb' \
+ 'aZ0sMIWezHIkxuo1iROMeT+jtCubn7ragaN6AX7nMpxYUH9+mYZZs/fyElt6wCviVhTI' \
+ 'zM+u7VdQsnZttOOlQfogHdL+SpeAft0DsfJjlcgQnsLlHQKv6aPqCPYUST2nE7RyW/Ex' \
+ 'PrMxLtOWt0/j8RYHbwwqvyeZqBz3ESBgrS9c5tBdBfauwYUV/E7gPLOU3OZFw9ue7o+z' \
+ 'wzoTZqW6Xouy5wtWvSLQSLT5XwOslmQz8QMBxD0AQyDfEFGsBCWzmbTgKv9uqrBjubsS' \
+ 'Taja+Cf9kMo== dummy@gitlab.com'
+ end
+ end
+
+ factory :ecdsa_key_256 do
+ key do
+ 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYA' \
+ 'AABBBJZmkzTgY0fiCQ+DVReyH/fFwTFz0XoR3RUO0u+199H19KFw7mNPxRSMOVS7tEtO' \
+ 'Nj3Q7FcZXfqthHvgAzDiHsc= dummy@gitlab.com'
+ end
+ end
+
+ factory :ed25519_key_256 do
+ key do
+ 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETnVTgzqC1gatgSlC4zH6aYt2CAQzgJOhDRvf59ohL6 dummy@gitlab.com'
+ end
+ end
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 1bc530d06db..cbec716d6ea 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -68,6 +68,17 @@ FactoryGirl.define do
merge_user author
end
+ after(:build) do |merge_request|
+ target_project = merge_request.target_project
+ source_project = merge_request.source_project
+
+ # Fake `write_ref` if we don't have repository
+ # We have too many existing tests replying on this behaviour
+ unless [target_project, source_project].all?(&:repository_exists?)
+ allow(merge_request).to receive(:write_ref)
+ end
+ end
+
factory :merged_merge_request, traits: [:merged]
factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:opened]
diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb
index 2f75bf12cd7..b5298b2f969 100644
--- a/spec/factories/milestones.rb
+++ b/spec/factories/milestones.rb
@@ -7,6 +7,7 @@ FactoryGirl.define do
group nil
project_id nil
group_id nil
+ parent nil
end
trait :active do
@@ -26,6 +27,9 @@ FactoryGirl.define do
milestone.project = evaluator.project
elsif evaluator.project_id
milestone.project_id = evaluator.project_id
+ elsif evaluator.parent
+ id = evaluator.parent.id
+ evaluator.parent.is_a?(Group) ? board.group_id = id : evaluator.project_id = id
else
milestone.project = create(:project)
end
diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb
new file mode 100644
index 00000000000..8d124dc2381
--- /dev/null
+++ b/spec/factories/project_auto_devops.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :project_auto_devops do
+ project
+ enabled true
+ domain "example.com"
+ end
+end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 3f8e7030b1c..7493b0a8b35 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -8,12 +8,47 @@ FactoryGirl.define do
factory :project, class: 'Project' do
sequence(:name) { |n| "project#{n}" }
path { name.downcase.gsub(/\s/, '_') }
- namespace
- creator
-
# Behaves differently to nil due to cache_has_external_issue_tracker
has_external_issue_tracker false
+ # Associations
+ namespace
+ creator { group ? create(:user) : namespace&.owner }
+
+ # Nest Project Feature attributes
+ transient do
+ wiki_access_level ProjectFeature::ENABLED
+ builds_access_level ProjectFeature::ENABLED
+ snippets_access_level ProjectFeature::ENABLED
+ issues_access_level ProjectFeature::ENABLED
+ merge_requests_access_level ProjectFeature::ENABLED
+ repository_access_level ProjectFeature::ENABLED
+ end
+
+ after(:create) do |project, evaluator|
+ # Builds and MRs can't have higher visibility level than repository access level.
+ builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min
+ merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min
+
+ project.project_feature.update_columns(
+ wiki_access_level: evaluator.wiki_access_level,
+ builds_access_level: builds_access_level,
+ snippets_access_level: evaluator.snippets_access_level,
+ issues_access_level: evaluator.issues_access_level,
+ merge_requests_access_level: merge_requests_access_level,
+ repository_access_level: evaluator.repository_access_level)
+
+ # Normally the class Projects::CreateService is used for creating
+ # projects, and this class takes care of making sure the owner and current
+ # user have access to the project. Our specs don't use said service class,
+ # thus we must manually refresh things here.
+ unless project.group || project.pending_delete
+ project.add_master(project.owner)
+ end
+
+ project.group&.refresh_members_authorized_projects
+ end
+
trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC
end
@@ -46,6 +81,10 @@ FactoryGirl.define do
archived true
end
+ trait :hashed do
+ storage_version Project::LATEST_STORAGE_VERSION
+ end
+
trait :access_requestable do
request_access_enabled true
end
@@ -62,35 +101,31 @@ FactoryGirl.define do
# Test repository - https://gitlab.com/gitlab-org/gitlab-test
trait :repository do
- path { 'gitlabhq' }
-
test_repo
transient do
- create_template nil
+ create_templates nil
end
after :create do |project, evaluator|
- if evaluator.create_template
- args = evaluator.create_template
-
- project.add_user(args[:user], args[:access])
+ if evaluator.create_templates
+ templates_path = "#{evaluator.create_templates}_templates"
project.repository.create_file(
- args[:user],
- ".gitlab/#{args[:path]}/bug.md",
+ project.creator,
+ ".gitlab/#{templates_path}/bug.md",
'something valid',
message: 'test 3',
branch_name: 'master')
project.repository.create_file(
- args[:user],
- ".gitlab/#{args[:path]}/template_test.md",
+ project.creator,
+ ".gitlab/#{templates_path}/template_test.md",
'template_test',
message: 'test 1',
branch_name: 'master')
project.repository.create_file(
- args[:user],
- ".gitlab/#{args[:path]}/feature_proposal.md",
+ project.creator,
+ ".gitlab/#{templates_path}/feature_proposal.md",
'feature_proposal',
message: 'test 2',
branch_name: 'master')
@@ -142,44 +177,6 @@ FactoryGirl.define do
trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED }
trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED }
trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE }
-
- # Nest Project Feature attributes
- transient do
- wiki_access_level ProjectFeature::ENABLED
- builds_access_level ProjectFeature::ENABLED
- snippets_access_level ProjectFeature::ENABLED
- issues_access_level ProjectFeature::ENABLED
- merge_requests_access_level ProjectFeature::ENABLED
- repository_access_level ProjectFeature::ENABLED
- end
-
- after(:create) do |project, evaluator|
- # Builds and MRs can't have higher visibility level than repository access level.
- builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min
- merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min
-
- project.project_feature
- .update_attributes!(
- wiki_access_level: evaluator.wiki_access_level,
- builds_access_level: builds_access_level,
- snippets_access_level: evaluator.snippets_access_level,
- issues_access_level: evaluator.issues_access_level,
- merge_requests_access_level: merge_requests_access_level,
- repository_access_level: evaluator.repository_access_level
- )
-
- # Normally the class Projects::CreateService is used for creating
- # projects, and this class takes care of making sure the owner and current
- # user have access to the project. Our specs don't use said service class,
- # thus we must manually refresh things here.
- owner = project.owner
-
- if owner && owner.is_a?(User) && !project.pending_delete
- project.members.create!(user: owner, access_level: Gitlab::Access::MASTER)
- end
-
- project.group&.refresh_members_authorized_projects
- end
end
# Project with empty repository
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index e60fe713bc3..4000cd085b7 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -1,5 +1,5 @@
FactoryGirl.define do
- factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator, :resource_owner] do
+ factory :user, aliases: [:author, :assignee, :recipient, :owner, :resource_owner] do
email { generate(:email) }
name { generate(:name) }
username { generate(:username) }
@@ -8,6 +8,10 @@ FactoryGirl.define do
confirmation_token { nil }
can_create_group true
+ after(:stub) do |user|
+ user.notification_email = user.email
+ end
+
before(:create) do |user|
user.ensure_rss_token
end
diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb
index 07430ecd6e0..1215908f5ea 100644
--- a/spec/features/admin/admin_active_tab_spec.rb
+++ b/spec/features/admin/admin_active_tab_spec.rb
@@ -7,15 +7,15 @@ RSpec.describe 'admin active tab' do
shared_examples 'page has active tab' do |title|
it "activates #{title} tab" do
- expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1)
- expect(page.find('.layout-nav li.active')).to have_content(title)
+ expect(page).to have_selector('.nav-sidebar .sidebar-top-level-items > li.active', count: 1)
+ expect(page.find('.nav-sidebar .sidebar-top-level-items > li.active')).to have_content(title)
end
end
shared_examples 'page has active sub tab' do |title|
it "activates #{title} sub tab" do
- expect(page).to have_selector('.sub-nav li.active', count: 1)
- expect(page.find('.sub-nav li.active')).to have_content(title)
+ expect(page).to have_selector('.sidebar-sub-level-items > li.active', count: 2)
+ expect(page.all('.sidebar-sub-level-items > li.active')[1]).to have_content(title)
end
end
diff --git a/spec/features/admin/admin_browses_logs_spec.rb b/spec/features/admin/admin_browses_logs_spec.rb
index 3e3404dfdac..02f50d7e27f 100644
--- a/spec/features/admin/admin_browses_logs_spec.rb
+++ b/spec/features/admin/admin_browses_logs_spec.rb
@@ -8,8 +8,10 @@ describe 'Admin browses logs' do
it 'shows available log files' do
visit admin_logs_path
- expect(page).to have_content 'test.log'
- expect(page).to have_content 'githost.log'
- expect(page).to have_content 'application.log'
+ expect(page).to have_link 'application.log'
+ expect(page).to have_link 'githost.log'
+ expect(page).to have_link 'test.log'
+ expect(page).to have_link 'sidekiq.log'
+ expect(page).to have_link 'repocheck.log'
end
end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 30fcb334b60..91f08dbad5d 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Admin::Hooks' do
+describe 'Admin::Hooks', :js do
before do
@project = create(:project)
sign_in(create(:admin))
@@ -12,7 +12,7 @@ describe 'Admin::Hooks' do
it 'is ok' do
visit admin_root_path
- page.within '.layout-nav' do
+ page.within '.nav-sidebar' do
click_on 'Hooks'
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 77710f80036..f4f2505d436 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -36,6 +36,14 @@ describe "Admin::Projects" do
expect(page).to have_content(archived_project.name)
expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
end
+
+ it 'renders only archived projects', js: true do
+ find(:css, '#sort-projects-dropdown').click
+ click_link 'Show archived projects only'
+
+ expect(page).to have_content(archived_project.name)
+ expect(page).not_to have_content(project.name)
+ end
end
describe "GET /admin/projects/:namespace_id/:id" do
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index c9591a7d854..c490dce7ab0 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -35,6 +35,7 @@ feature 'Admin updates settings' do
fill_in 'Help page text', with: 'Example text'
check 'Hide marketing-related entries from help'
fill_in 'Support page URL', with: 'http://example.com/help'
+ uncheck 'Project export enabled'
click_button 'Save'
expect(current_application_settings.gravatar_enabled).to be_falsey
@@ -42,11 +43,12 @@ feature 'Admin updates settings' do
expect(current_application_settings.help_page_text).to eq "Example text"
expect(current_application_settings.help_page_hide_commercial_content).to be_truthy
expect(current_application_settings.help_page_support_url).to eq "http://example.com/help"
+ expect(current_application_settings.project_export_enabled).to be_falsey
expect(page).to have_content "Application settings saved successfully"
end
scenario 'Change Slack Notifications Service template settings' do
- click_link 'Service Templates'
+ first(:link, 'Service Templates').click
click_link 'Slack notifications'
fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user'
@@ -72,11 +74,27 @@ feature 'Admin updates settings' do
context 'sign-in restrictions', :js do
it 'de-activates oauth sign-in source' do
find('.btn', text: 'GitLab.com').click
-
+
expect(find('.btn', text: 'GitLab.com')).not_to have_css('.active')
end
end
+ scenario 'Change Keys settings' do
+ select 'Are forbidden', from: 'RSA SSH keys'
+ select 'Are allowed', from: 'DSA SSH keys'
+ select 'Must be at least 384 bits', from: 'ECDSA SSH keys'
+ select 'Are forbidden', from: 'ED25519 SSH keys'
+ click_on 'Save'
+
+ forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE.to_s
+
+ expect(page).to have_content 'Application settings saved successfully'
+ expect(find_field('RSA SSH keys').value).to eq(forbidden)
+ expect(find_field('DSA SSH keys').value).to eq('0')
+ expect(find_field('ECDSA SSH keys').value).to eq('384')
+ expect(find_field('ED25519 SSH keys').value).to eq(forbidden)
+ end
+
def check_all_events
page.check('Active')
page.check('Push')
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 79069bbca8e..9ce687afb31 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -41,6 +41,8 @@ describe "User Feed" do
target_project: project,
description: "Here is the fix: ![an image](image.png)")
end
+ let(:push_event) { create(:push_event, project: project, author: user) }
+ let!(:push_event_payload) { create(:push_event_payload, event: push_event) }
before do
project.team << [user, :master]
@@ -70,6 +72,10 @@ describe "User Feed" do
it 'has XHTML summaries in merge request descriptions' do
expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/
end
+
+ it 'has push event commit ID' do
+ expect(body).to have_content(Commit.truncate_sha(push_event.commit_id))
+ end
end
end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index a6ad5981f8f..c480b5b7e34 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -8,8 +8,8 @@ describe 'Issue Boards add issue modal', :js do
let!(:label) { create(:label, project: project) }
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: label, position: 1) }
- let!(:issue) { create(:issue, project: project) }
- let!(:issue2) { create(:issue, project: project) }
+ let!(:issue) { create(:issue, project: project, title: 'abc', description: 'def') }
+ let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') }
before do
project.team << [user, :master]
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index ce458431c55..e010b5f3444 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -13,6 +13,8 @@ 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)
+
sign_in(user)
end
@@ -71,15 +73,15 @@ describe 'Issue Boards', js: true do
let!(:list2) { create(:list, board: board, label: development, position: 1) }
let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
- let!(:issue1) { create(:labeled_issue, project: project, assignees: [user], labels: [planning], relative_position: 8) }
- let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) }
- let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) }
- let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) }
- let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone, relative_position: 4) }
- let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development], relative_position: 3) }
- let!(:issue7) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) }
- let!(:issue8) { create(:closed_issue, project: project) }
- let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting], relative_position: 1) }
+ let!(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) }
+ let!(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) }
+ let!(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) }
+ let!(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) }
+ let!(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) }
+ let!(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) }
+ let!(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) }
+ let!(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') }
+ let!(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) }
before do
visit project_board_path(project, board)
@@ -145,6 +147,8 @@ describe 'Issue Boards', js: true do
click_button 'Add list'
wait_for_requests
+ find('.dropdown-menu-close').click
+
page.within(find('.board:nth-child(2)')) do
find('.board-delete').click
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 8d3d4ff8773..c3bf50ef9d1 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -15,10 +15,12 @@ describe 'Issue Boards', js: true do
let!(:list) { create(:list, board: board, label: development, position: 0) }
let(:card) { find('.board:nth-child(2)').first('.card') }
- before do
- Timecop.freeze
+ around do |example|
+ Timecop.freeze { example.run }
+ end
- project.team << [user, :master]
+ before do
+ project.add_master(user)
sign_in(user)
@@ -26,10 +28,6 @@ describe 'Issue Boards', js: true do
wait_for_requests
end
- after do
- Timecop.return
- end
-
it 'shows sidebar when clicking issue' do
click_card(card)
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index 64fbc80cb81..4fc6956d111 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
feature 'Contributions Calendar', :js do
let(:user) { create(:user) }
- let(:contributed_project) { create(:project, :public) }
+ let(:contributed_project) { create(:project, :public, :repository) }
let(:issue_note) { create(:note, project: contributed_project) }
# Ex/ Sunday Jan 1, 2016
@@ -42,14 +42,14 @@ feature 'Contributions Calendar', :js do
end
def push_code_contribution
- push_params = {
- project: contributed_project,
- action: Event::PUSHED,
- author_id: user.id,
- data: { commit_count: 3 }
- }
-
- Event.create(push_params)
+ event = create(:push_event, project: contributed_project, author: user)
+
+ create(:push_event_payload,
+ event: event,
+ commit_from: '11f9ac0a48b62cef25eedede4c1819964f08d5ce',
+ commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ commit_count: 3,
+ ref: 'master')
end
def note_comment_contribution
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 0c9fcc60d30..479fb713297 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -203,105 +203,4 @@ describe 'Commits' do
end
end
end
-
- describe 'GPG signed commits', :js do
- it 'changes from unverified to verified when the user changes his email to match the gpg key' do
- user = create :user, email: 'unrelated.user@example.org'
- project.team << [user, :master]
-
- Sidekiq::Testing.inline! do
- create :gpg_key, key: GpgHelpers::User1.public_key, user: user
- end
-
- sign_in(user)
-
- visit project_commits_path(project, :'signed-commits')
-
- within '#commits-list' do
- expect(page).to have_content 'Unverified'
- expect(page).not_to have_content 'Verified'
- end
-
- # user changes his email which makes the gpg key verified
- Sidekiq::Testing.inline! do
- user.skip_reconfirmation!
- user.update_attributes!(email: GpgHelpers::User1.emails.first)
- end
-
- visit project_commits_path(project, :'signed-commits')
-
- within '#commits-list' do
- expect(page).to have_content 'Unverified'
- expect(page).to have_content 'Verified'
- end
- end
-
- it 'changes from unverified to verified when the user adds the missing gpg key' do
- user = create :user, email: GpgHelpers::User1.emails.first
- project.team << [user, :master]
-
- sign_in(user)
-
- visit project_commits_path(project, :'signed-commits')
-
- within '#commits-list' do
- expect(page).to have_content 'Unverified'
- expect(page).not_to have_content 'Verified'
- end
-
- # user adds the gpg key which makes the signature valid
- Sidekiq::Testing.inline! do
- create :gpg_key, key: GpgHelpers::User1.public_key, user: user
- end
-
- visit project_commits_path(project, :'signed-commits')
-
- within '#commits-list' do
- expect(page).to have_content 'Unverified'
- expect(page).to have_content 'Verified'
- end
- end
-
- it 'shows popover badges' do
- gpg_user = create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard'
- Sidekiq::Testing.inline! do
- create :gpg_key, key: GpgHelpers::User1.public_key, user: gpg_user
- end
-
- user = create :user
- project.team << [user, :master]
-
- sign_in(user)
- visit project_commits_path(project, :'signed-commits')
-
- # unverified signature
- click_on 'Unverified', match: :first
- within '.popover' do
- expect(page).to have_content 'This commit was signed with an unverified signature.'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
- end
-
- # verified and the gpg user has a gitlab profile
- click_on 'Verified', match: :first
- within '.popover' do
- expect(page).to have_content 'This commit was signed with a verified signature.'
- expect(page).to have_content 'Nannie Bernhard'
- expect(page).to have_content '@nannie.bernhard'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
- end
-
- # verified and the gpg user's profile doesn't exist anymore
- gpg_user.destroy!
-
- visit project_commits_path(project, :'signed-commits')
-
- click_on 'Verified', match: :first
- within '.popover' do
- expect(page).to have_content 'This commit was signed with a verified signature.'
- expect(page).to have_content 'Nannie Bernhard'
- expect(page).to have_content 'nannie.bernhard@example.com'
- expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
- end
- end
- end
end
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 5c60cca10b9..bfe9dac3bd4 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -24,6 +24,12 @@ feature 'Cycle Analytics', js: true do
expect(page).to have_content('Introducing Cycle Analytics')
end
+ it 'shows pipeline summary' do
+ expect(new_issues_counter).to have_content('-')
+ expect(commits_counter).to have_content('-')
+ expect(deploys_counter).to have_content('-')
+ end
+
it 'shows active stage with empty message' do
expect(page).to have_selector('.stage-nav-item.active', text: 'Issue')
expect(page).to have_content("We don't have enough data to show this stage.")
@@ -42,6 +48,12 @@ feature 'Cycle Analytics', js: true do
visit project_cycle_analytics_path(project)
end
+ it 'shows pipeline summary' do
+ expect(new_issues_counter).to have_content('1')
+ expect(commits_counter).to have_content('2')
+ expect(deploys_counter).to have_content('1')
+ end
+
it 'shows data on each stage' do
expect_issue_to_be_present
@@ -63,6 +75,20 @@ feature 'Cycle Analytics', js: true do
click_stage('Production')
expect_issue_to_be_present
end
+
+ context "when I change the time period observed" do
+ before do
+ _two_weeks_old_issue = create(:issue, project: project, created_at: 2.weeks.ago)
+
+ click_button('Last 30 days')
+ click_link('Last 7 days')
+ wait_for_requests
+ end
+
+ it 'shows only relevant data' do
+ expect(new_issues_counter).to have_content('1')
+ end
+ end
end
context "when my preferred language is Spanish" do
@@ -109,6 +135,18 @@ feature 'Cycle Analytics', js: true do
end
end
+ def new_issues_counter
+ find(:xpath, "//p[contains(text(),'New Issue')]/preceding-sibling::h3")
+ end
+
+ def commits_counter
+ find(:xpath, "//p[contains(text(),'Commits')]/preceding-sibling::h3")
+ end
+
+ def deploys_counter
+ find(:xpath, "//p[contains(text(),'Deploy')]/preceding-sibling::h3")
+ end
+
def expect_issue_to_be_present
expect(find('.stage-events')).to have_content(issue.title)
expect(find('.stage-events')).to have_content(issue.author.name)
diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb
index 067e4337e6a..08d8cc7922b 100644
--- a/spec/features/dashboard/active_tab_spec.rb
+++ b/spec/features/dashboard/active_tab_spec.rb
@@ -7,9 +7,8 @@ RSpec.describe 'Dashboard Active Tab', js: true do
shared_examples 'page has active tab' do |title|
it "#{title} tab" do
- find('.global-dropdown-toggle').trigger('click')
- expect(page).to have_selector('.global-dropdown-menu li.active', count: 1)
- expect(find('.global-dropdown-menu li.active')).to have_content(title)
+ expect(page).to have_selector('.navbar-sub-nav li.active', count: 1)
+ expect(find('.navbar-sub-nav li.active')).to have_content(title)
end
end
@@ -21,27 +20,19 @@ RSpec.describe 'Dashboard Active Tab', js: true do
it_behaves_like 'page has active tab', 'Projects'
end
- context 'on dashboard issues' do
- before do
- visit issues_dashboard_path
- end
-
- it_behaves_like 'page has active tab', 'Issues'
- end
-
- context 'on dashboard merge requests' do
+ context 'on dashboard groups' do
before do
- visit merge_requests_dashboard_path
+ visit dashboard_groups_path
end
- it_behaves_like 'page has active tab', 'Merge Requests'
+ it_behaves_like 'page has active tab', 'Groups'
end
- context 'on dashboard groups' do
+ context 'on activity projects' do
before do
- visit dashboard_groups_path
+ visit activity_dashboard_path
end
- it_behaves_like 'page has active tab', 'Groups'
+ it_behaves_like 'page has active tab', 'Activity'
end
end
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index 4917dfcf1d1..bd115785646 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -17,33 +17,25 @@ feature 'Dashboard > Activity' do
end
context 'event filters', :js do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:merge_request) do
create(:merge_request, author: user, source_project: project, target_project: project)
end
- let(:push_event_data) do
- {
- before: Gitlab::Git::BLANK_SHA,
- after: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e',
- ref: 'refs/heads/new_design',
- user_id: user.id,
- user_name: user.name,
- repository: {
- name: project.name,
- url: 'localhost/rubinius',
- description: '',
- homepage: 'localhost/rubinius',
- private: true
- }
- }
- end
-
let(:note) { create(:note, project: project, noteable: merge_request) }
let!(:push_event) do
- create(:event, :pushed, data: push_event_data, project: project, author: user)
+ event = create(:push_event, project: project, author: user)
+
+ create(:push_event_payload,
+ event: event,
+ action: :created,
+ commit_to: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e',
+ ref: 'new_design',
+ commit_count: 1)
+
+ event
end
let!(:merged_event) do
diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb
index 814ec0e59c7..e8d699ff5e0 100644
--- a/spec/features/dashboard/archived_projects_spec.rb
+++ b/spec/features/dashboard/archived_projects_spec.rb
@@ -26,6 +26,13 @@ RSpec.describe 'Dashboard Archived Project' do
expect(page).to have_link(archived_project.name)
end
+ it 'renders only archived projects' do
+ click_link 'Show archived projects only'
+
+ expect(page).to have_content(archived_project.name)
+ expect(page).not_to have_content(project.name)
+ end
+
it 'searchs archived projects', :js do
click_button 'Last updated'
click_link 'Show archived projects'
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 06a43909053..0613c158c54 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -84,25 +84,11 @@ feature 'Dashboard Projects' do
end
context 'last push widget' do
- let(:push_event_data) do
- {
- before: Gitlab::Git::BLANK_SHA,
- after: '0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e',
- ref: 'refs/heads/feature',
- user_id: user.id,
- user_name: user.name,
- repository: {
- name: project.name,
- url: 'localhost/rubinius',
- description: '',
- homepage: 'localhost/rubinius',
- private: true
- }
- }
- end
- let!(:push_event) { create(:event, :pushed, data: push_event_data, project: project, author: user) }
-
before do
+ event = create(:push_event, project: project, author: user)
+
+ create(:push_event_payload, event: event, ref: 'feature', action: :created)
+
visit dashboard_projects_path
end
@@ -115,9 +101,9 @@ feature 'Dashboard Projects' do
expect(page).to have_selector('.merge-request-form')
expect(current_path).to eq project_new_merge_request_path(project)
- expect(find('#merge_request_target_project_id').value).to eq project.id.to_s
- expect(find('input#merge_request_source_branch').value).to eq 'feature'
- expect(find('input#merge_request_target_branch').value).to eq 'master'
+ expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s
+ expect(find('input#merge_request_source_branch', visible: false).value).to eq 'feature'
+ expect(find('input#merge_request_target_branch', visible: false).value).to eq 'master'
end
end
end
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index 5f1f0c10339..e41bd7a8419 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -50,6 +50,6 @@ feature 'Dashboard shortcuts', :js do
end
def check_page_title(title)
- expect(find('.header-content .title')).to have_content(title)
+ expect(find('.breadcrumbs-sub-title')).to have_content(title)
end
end
diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb
index 2cd06258e22..e1c74a24890 100644
--- a/spec/features/explore/new_menu_spec.rb
+++ b/spec/features/explore/new_menu_spec.rb
@@ -74,7 +74,7 @@ feature 'Top Plus Menu', :js do
expect(page).to have_content('Title')
end
- scenario 'Click on New subgroup shows new group page' do
+ scenario 'Click on New subgroup shows new group page', :nested_groups do
visit group_path(group)
click_topmenuitem("New subgroup")
diff --git a/spec/features/groups/group_name_toggle_spec.rb b/spec/features/groups/group_name_toggle_spec.rb
deleted file mode 100644
index a7b8b702ab7..00000000000
--- a/spec/features/groups/group_name_toggle_spec.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require 'spec_helper'
-
-feature 'Group name toggle', js: true do
- let(:group) { create(:group) }
- let(:nested_group_1) { create(:group, parent: group) }
- let(:nested_group_2) { create(:group, parent: nested_group_1) }
- let(:nested_group_3) { create(:group, parent: nested_group_2) }
-
- SMALL_SCREEN = 300
-
- before do
- sign_in(create(:user))
- end
-
- it 'is not present if enough horizontal space' do
- visit group_path(nested_group_3)
-
- container_width = page.evaluate_script("$('.title-container')[0].offsetWidth")
- title_width = page.evaluate_script("$('.title')[0].offsetWidth")
-
- expect(container_width).to be > title_width
- expect(page).not_to have_css('.group-name-toggle')
- end
-
- it 'is present if the title is longer than the container', :nested_groups do
- visit group_path(nested_group_3)
- title_width = page.evaluate_script("$('.title')[0].offsetWidth")
-
- page_height = page.current_window.size[1]
- page.current_window.resize_to(SMALL_SCREEN, page_height)
-
- find('.group-name-toggle')
- container_width = page.evaluate_script("$('.title-container')[0].offsetWidth")
-
- expect(title_width).to be > container_width
- end
-
- it 'should show the full group namespace when toggled', :nested_groups do
- page_height = page.current_window.size[1]
- page.current_window.resize_to(SMALL_SCREEN, page_height)
- visit group_path(nested_group_3)
-
- expect(page).not_to have_content(group.name)
- expect(page).to have_css('.group-path.hidable', visible: false)
-
- click_button '...'
-
- expect(page).to have_content(group.name)
- expect(page).to have_css('.group-path.hidable', visible: true)
- end
-end
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index acb21eab03f..b83bad3befb 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -57,7 +57,7 @@ feature 'Edit group settings' do
TestEnv.clean_test_path
end
- after(:example) do
+ after do
TestEnv.clean_test_path
end
@@ -65,14 +65,14 @@ feature 'Edit group settings' do
update_path(new_group_path)
visit new_project_full_path
expect(current_path).to eq(new_project_full_path)
- expect(find('h1.title')).to have_content(project.path)
+ expect(find('.breadcrumbs')).to have_content(project.path)
end
scenario 'the old project path redirects to the new path' do
update_path(new_group_path)
visit old_project_full_path
expect(current_path).to eq(new_project_full_path)
- expect(find('h1.title')).to have_content(project.path)
+ expect(find('.breadcrumbs')).to have_content(project.path)
end
end
end
diff --git a/spec/features/groups/members/request_access_spec.rb b/spec/features/groups/members/request_access_spec.rb
index 1f3c7fd3859..10389a74703 100644
--- a/spec/features/groups/members/request_access_spec.rb
+++ b/spec/features/groups/members/request_access_spec.rb
@@ -51,7 +51,7 @@ feature 'Groups > Members > Request access' do
expect(group.requesters.exists?(user_id: user)).to be_truthy
- click_link 'Members'
+ first(:link, 'Members').click
page.within('.content') do
expect(page).not_to have_content(user.name)
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index c2241feb9f7..2577d98df6f 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Group merge requests page' do
+ include FilteredSearchHelpers
+
let(:path) { merge_requests_group_path(group) }
let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: 'this is my created issuable') }
@@ -23,7 +25,7 @@ feature 'Group merge requests page' do
end
it 'ignores archived merge request count badges in navbar' do
- expect( page.find('[title="Merge Requests"] span.badge.count').text).to eq("1")
+ expect( page.find('[aria-label="Merge Requests"] span.badge.count').text).to eq("1")
end
it 'ignores archived merge request count badges in state-filters' do
@@ -33,4 +35,17 @@ feature 'Group merge requests page' do
expect(page.find('#state-all span.badge').text).to eq("1")
end
end
+
+ context 'group filtered search', :js do
+ let(:access_level) { ProjectFeature::ENABLED }
+ let(:user) { user_in_group }
+ let(:user2) { user_outside_group }
+
+ it 'filters by assignee only group users' do
+ filtered_search.set('assignee:')
+
+ expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name)
+ expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
+ end
+ end
end
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 574bbe0e0e1..56144d17d4f 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -5,14 +5,12 @@ feature 'Group milestones', :js do
let!(:project) { create(:project_empty_repo, group: group) }
let(:user) { create(:group_member, :master, user: create(:user), group: group ).user }
- before do
- Timecop.freeze
-
- sign_in(user)
+ around do |example|
+ Timecop.freeze { example.run }
end
- after do
- Timecop.return
+ before do
+ sign_in(user)
end
context 'create a milestone' do
@@ -37,12 +35,12 @@ feature 'Group milestones', :js do
context 'milestones list' do
let!(:other_project) { create(:project_empty_repo, group: group) }
- let!(:active_group_milestone) { create(:milestone, group: group, state: 'active') }
let!(:active_project_milestone1) { create(:milestone, project: project, state: 'active', title: 'v1.0') }
let!(:active_project_milestone2) { create(:milestone, project: other_project, state: 'active', title: 'v1.0') }
- let!(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') }
let!(:closed_project_milestone1) { create(:milestone, project: project, state: 'closed', title: 'v2.0') }
let!(:closed_project_milestone2) { create(:milestone, project: other_project, state: 'closed', title: 'v2.0') }
+ let!(:active_group_milestone) { create(:milestone, group: group, state: 'active') }
+ let!(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') }
before do
visit group_milestones_path(group)
@@ -60,5 +58,30 @@ feature 'Group milestones', :js do
expect(page).to have_selector("#milestone_#{active_group_milestone.id}", count: 1)
expect(page).to have_selector("#milestone_#{legacy_milestone.milestones.first.id}", count: 1)
end
+
+ it 'updates milestone' do
+ page.within(".milestones #milestone_#{active_group_milestone.id}") do
+ click_link('Edit')
+ end
+
+ page.within('.milestone-form') do
+ fill_in 'milestone_title', with: 'new title'
+ click_button('Update milestone')
+ end
+
+ expect(find('#content-body h2')).to have_content('new title')
+ end
+
+ it 'shows milestone detail and supports its edit' do
+ page.within(".milestones #milestone_#{active_group_milestone.id}") do
+ click_link(active_group_milestone.title)
+ end
+
+ page.within('.detail-page-header') do
+ click_link('Edit')
+ end
+
+ expect(page).to have_selector('.milestone-form')
+ end
end
end
diff --git a/spec/features/groups/share_lock_spec.rb b/spec/features/groups/share_lock_spec.rb
new file mode 100644
index 00000000000..8842d1391aa
--- /dev/null
+++ b/spec/features/groups/share_lock_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+feature 'Group share with group lock' do
+ given(:root_owner) { create(:user) }
+ given(:root_group) { create(:group) }
+
+ background do
+ root_group.add_owner(root_owner)
+ sign_in(root_owner)
+ end
+
+ context 'with a subgroup', :nested_groups do
+ given!(:subgroup) { create(:group, parent: root_group) }
+
+ context 'when enabling the parent group share with group lock' do
+ scenario 'the subgroup share with group lock becomes enabled' do
+ visit edit_group_path(root_group)
+ check 'group_share_with_group_lock'
+
+ click_on 'Save group'
+
+ expect(subgroup.reload.share_with_group_lock?).to be_truthy
+ end
+ end
+
+ context 'when disabling the parent group share with group lock (which was already enabled)' do
+ background do
+ visit edit_group_path(root_group)
+ check 'group_share_with_group_lock'
+ click_on 'Save group'
+ end
+
+ context 'and the subgroup share with group lock is enabled' do
+ scenario 'the subgroup share with group lock does not change' do
+ visit edit_group_path(root_group)
+ uncheck 'group_share_with_group_lock'
+
+ click_on 'Save group'
+
+ expect(subgroup.reload.share_with_group_lock?).to be_truthy
+ end
+ end
+
+ context 'but the subgroup share with group lock is disabled' do
+ background do
+ visit edit_group_path(subgroup)
+ uncheck 'group_share_with_group_lock'
+ click_on 'Save group'
+ end
+
+ scenario 'the subgroup share with group lock does not change' do
+ visit edit_group_path(root_group)
+ uncheck 'group_share_with_group_lock'
+
+ click_on 'Save group'
+
+ expect(subgroup.reload.share_with_group_lock?).to be_falsey
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index e59a484d992..4ec2e7e6012 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -104,18 +104,15 @@ feature 'Group' do
end
context 'as group owner' do
- let(:user) { create(:user) }
+ it 'creates a nested group' do
+ user = create(:user)
- before do
group.add_owner(user)
sign_out(:user)
sign_in(user)
visit subgroups_group_path(group)
click_link 'New Subgroup'
- end
-
- it 'creates a nested group' do
fill_in 'Group path', with: 'bar'
click_button 'Create group'
@@ -123,6 +120,16 @@ feature 'Group' do
expect(page).to have_content("Group 'bar' was successfully created.")
end
end
+
+ context 'when nested group feature is disabled' do
+ it 'renders 404' do
+ allow(Group).to receive(:supports_nested_groups?).and_return(false)
+
+ visit subgroups_group_path(group)
+
+ expect(page.status_code).to eq(404)
+ end
+ end
end
it 'checks permissions to avoid exposing groups by parent_id' do
@@ -151,7 +158,7 @@ feature 'Group' do
expect(page).to have_content 'successfully updated'
expect(find('#group_name').value).to eq(new_name)
- page.within ".navbar-gitlab" do
+ page.within ".breadcrumbs" do
expect(page).to have_content new_name
end
end
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index bd4f233cba9..93be3b066ee 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -50,7 +50,7 @@ describe 'Help Pages' do
it 'hides the version check image if the image request fails' do
# We use '--load-images=yes' with poltergeist so the image fails to load
- expect(find('.js-version-status-badge', visible: false)).not_to be_visible
+ expect(page).to have_selector('.js-version-status-badge', visible: false)
end
end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 134e618feac..a29acb30163 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -70,13 +70,13 @@ describe 'Awards Emoji' do
it 'toggles the smiley emoji on a note', js: true do
toggle_smiley_emoji(true)
- within('.note-awards') do
+ within('.note-body') do
expect(find(emoji_counter)).to have_text("1")
end
toggle_smiley_emoji(false)
- within('.note-awards') do
+ within('.note-body') do
expect(page).not_to have_selector(emoji_counter)
end
end
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index 847e3856ba5..b2229b44f99 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -353,7 +353,7 @@ feature 'Issues > Labels bulk assignment' do
context 'cannot bulk assign labels' do
it do
- expect(page).not_to have_button 'Edit Issues'
+ expect(page).not_to have_button 'Edit issues'
expect(page).not_to have_css '.check-all-issues'
expect(page).not_to have_css '.issue-check'
end
@@ -411,7 +411,7 @@ feature 'Issues > Labels bulk assignment' do
def enable_bulk_update
visit project_issues_path(project)
- click_button 'Edit Issues'
+ click_button 'Edit issues'
end
def disable_bulk_update
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index a69bd8a09b7..1c4649d0ba9 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -134,8 +134,10 @@ describe 'Dropdown assignee', :js do
it 'fills in the assignee username when the assignee has not been filtered' do
click_assignee(user_jacob.name)
+ wait_for_requests
+
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect_tokens([{ name: 'assignee', value: "@#{user_jacob.username}" }])
+ expect_tokens([assignee_token(user_jacob.name)])
expect_filtered_search_input_empty
end
@@ -143,8 +145,10 @@ describe 'Dropdown assignee', :js do
filtered_search.send_keys('roo')
click_assignee(user.name)
+ wait_for_requests
+
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_tokens([assignee_token(user.name)])
expect_filtered_search_input_empty
end
@@ -152,7 +156,7 @@ describe 'Dropdown assignee', :js do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect_tokens([{ name: 'assignee', value: 'none' }])
+ expect_tokens([assignee_token('none')])
expect_filtered_search_input_empty
end
end
@@ -171,7 +175,7 @@ describe 'Dropdown assignee', :js do
find('#js-dropdown-assignee .filter-dropdown-item', text: user.username).click
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect_tokens([{ name: 'assignee', value: user.username }])
+ expect_tokens([assignee_token(user.username)])
expect_filtered_search_input_empty
end
end
@@ -200,6 +204,12 @@ describe 'Dropdown assignee', :js do
expect(page).to have_css(js_dropdown_assignee, visible: true)
end
+
+ it 'opens assignee dropdown with existing my-reaction' do
+ filtered_search.set('my-reaction:star assignee:')
+
+ expect(page).to have_css(js_dropdown_assignee, visible: true)
+ end
end
describe 'caching requests' do
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 4bbf18e1dbe..3cec59050ab 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -6,7 +6,7 @@ describe 'Dropdown author', js: true do
let!(:project) { create(:project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
let!(:user_john) { create(:user, name: 'John', username: 'th0mas') }
- let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') }
+ let!(:user_jacob) { create(:user, name: 'Jacob', username: 'ooter32') }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_author) { '#js-dropdown-author' }
@@ -82,31 +82,31 @@ describe 'Dropdown author', js: true do
end
it 'filters by name' do
- send_keys_to_filtered_search('ja')
+ send_keys_to_filtered_search('jac')
expect(dropdown_author_size).to eq(1)
end
it 'filters by case insensitive name' do
- send_keys_to_filtered_search('Ja')
+ send_keys_to_filtered_search('Jac')
expect(dropdown_author_size).to eq(1)
end
it 'filters by username with symbol' do
- send_keys_to_filtered_search('@ot')
+ send_keys_to_filtered_search('@oot')
expect(dropdown_author_size).to eq(2)
end
it 'filters by username without symbol' do
- send_keys_to_filtered_search('ot')
+ send_keys_to_filtered_search('oot')
expect(dropdown_author_size).to eq(2)
end
it 'filters by case insensitive username without symbol' do
- send_keys_to_filtered_search('OT')
+ send_keys_to_filtered_search('OOT')
expect(dropdown_author_size).to eq(2)
end
@@ -121,16 +121,20 @@ describe 'Dropdown author', js: true do
it 'fills in the author username when the author has not been filtered' do
click_author(user_jacob.name)
+ wait_for_requests
+
expect(page).to have_css(js_dropdown_author, visible: false)
- expect_tokens([{ name: 'author', value: "@#{user_jacob.username}" }])
+ expect_tokens([author_token(user_jacob.name)])
expect_filtered_search_input_empty
end
it 'fills in the author username when the author has been filtered' do
click_author(user.name)
+ wait_for_requests
+
expect(page).to have_css(js_dropdown_author, visible: false)
- expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_tokens([author_token(user.name)])
expect_filtered_search_input_empty
end
end
@@ -149,7 +153,7 @@ describe 'Dropdown author', js: true do
find('#js-dropdown-author .filter-dropdown-item', text: user.username).click
expect(page).to have_css(js_dropdown_author, visible: false)
- expect_tokens([{ name: 'author', value: user.username }])
+ expect_tokens([author_token(user.username)])
expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
new file mode 100644
index 00000000000..44741bcc92d
--- /dev/null
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -0,0 +1,182 @@
+require 'rails_helper'
+
+describe 'Dropdown emoji', js: true do
+ include FilteredSearchHelpers
+
+ let!(:project) { create(:project, :public) }
+ let!(:user) { create(:user, name: 'administrator', username: 'root') }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:award_emoji_star) { create(:award_emoji, name: 'star', user: user, awardable: issue) }
+ let(:filtered_search) { find('.filtered-search') }
+ let(:js_dropdown_emoji) { '#js-dropdown-my-reaction' }
+
+ def send_keys_to_filtered_search(input)
+ input.split("").each do |i|
+ filtered_search.send_keys(i)
+ end
+
+ sleep 0.5
+ wait_for_requests
+ end
+
+ def dropdown_emoji_size
+ page.all('#js-dropdown-my-reaction .filter-dropdown .filter-dropdown-item').size
+ end
+
+ def click_emoji(text)
+ find('#js-dropdown-my-reaction .filter-dropdown .filter-dropdown-item', text: text).click
+ end
+
+ before do
+ project.team << [user, :master]
+ create_list(:award_emoji, 2, user: user, name: 'thumbsup')
+ create_list(:award_emoji, 1, user: user, name: 'thumbsdown')
+ create_list(:award_emoji, 3, user: user, name: 'star')
+ create_list(:award_emoji, 1, user: user, name: 'tea')
+ end
+
+ context 'when user not logged in' do
+ before do
+ visit project_issues_path(project)
+ end
+
+ describe 'behavior' do
+ it 'does not open when the search bar has my-reaction:' do
+ filtered_search.set('my-reaction:')
+
+ expect(page).not_to have_css(js_dropdown_emoji)
+ end
+ end
+ end
+
+ context 'when user loggged in' do
+ before do
+ sign_in(user)
+
+ visit project_issues_path(project)
+ end
+
+ describe 'behavior' do
+ it 'opens when the search bar has my-reaction:' do
+ filtered_search.set('my-reaction:')
+
+ expect(page).to have_css(js_dropdown_emoji, visible: true)
+ end
+
+ it 'closes when the search bar is unfocused' do
+ find('body').click()
+
+ expect(page).to have_css(js_dropdown_emoji, visible: false)
+ end
+
+ it 'should show loading indicator when opened' do
+ filtered_search.set('my-reaction:')
+
+ expect(page).to have_css('#js-dropdown-my-reaction .filter-dropdown-loading', visible: true)
+ end
+
+ it 'should hide loading indicator when loaded' do
+ send_keys_to_filtered_search('my-reaction:')
+
+ expect(page).not_to have_css('#js-dropdown-my-reaction .filter-dropdown-loading')
+ end
+
+ it 'should load all the emojis when opened' do
+ send_keys_to_filtered_search('my-reaction:')
+
+ expect(dropdown_emoji_size).to eq(4)
+ end
+
+ it 'shows the most populated emoji at top of dropdown' do
+ send_keys_to_filtered_search('my-reaction:')
+
+ expect(first('#js-dropdown-my-reaction li')).to have_content(award_emoji_star.name)
+ end
+ end
+
+ describe 'filtering' do
+ before do
+ filtered_search.set('my-reaction')
+ send_keys_to_filtered_search(':')
+ end
+
+ it 'filters by name' do
+ send_keys_to_filtered_search('up')
+
+ expect(dropdown_emoji_size).to eq(1)
+ end
+
+ it 'filters by case insensitive name' do
+ send_keys_to_filtered_search('Up')
+
+ expect(dropdown_emoji_size).to eq(1)
+ end
+ end
+
+ describe 'selecting from dropdown' do
+ before do
+ filtered_search.set('my-reaction')
+ send_keys_to_filtered_search(':')
+ end
+
+ it 'fills in the my-reaction name' do
+ click_emoji('thumbsup')
+
+ wait_for_requests
+
+ expect(page).to have_css(js_dropdown_emoji, visible: false)
+ expect_tokens([emoji_token('thumbsup')])
+ expect_filtered_search_input_empty
+ end
+ end
+
+ describe 'input has existing content' do
+ it 'opens my-reaction dropdown with existing search term' do
+ filtered_search.set('searchTerm my-reaction:')
+
+ expect(page).to have_css(js_dropdown_emoji, visible: true)
+ end
+
+ it 'opens my-reaction dropdown with existing assignee' do
+ filtered_search.set('assignee:@user my-reaction:')
+
+ expect(page).to have_css(js_dropdown_emoji, visible: true)
+ end
+
+ it 'opens my-reaction dropdown with existing label' do
+ filtered_search.set('label:~bug my-reaction:')
+
+ expect(page).to have_css(js_dropdown_emoji, visible: true)
+ end
+
+ it 'opens my-reaction dropdown with existing milestone' do
+ filtered_search.set('milestone:%v1.0 my-reaction:')
+
+ expect(page).to have_css(js_dropdown_emoji, visible: true)
+ end
+
+ it 'opens my-reaction dropdown with existing my-reaction' do
+ filtered_search.set('my-reaction:star my-reaction:')
+
+ expect(page).to have_css(js_dropdown_emoji, visible: true)
+ end
+ end
+
+ describe 'caching requests' do
+ it 'caches requests after the first load' do
+ filtered_search.set('my-reaction')
+ send_keys_to_filtered_search(':')
+ initial_size = dropdown_emoji_size
+
+ expect(initial_size).to be > 0
+
+ create_list(:award_emoji, 1, user: user, name: 'smile')
+ find('.filtered-search-box .clear-search').click
+ filtered_search.set('my-reaction')
+ send_keys_to_filtered_search(':')
+
+ expect(dropdown_emoji_size).to eq(initial_size)
+ end
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index 04d6dea4b8c..0183495a1db 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
describe 'Dropdown hint', :js do
include FilteredSearchHelpers
- let!(:project) { create(:project) }
+ let!(:project) { create(:project, :public) }
let!(:user) { create(:user) }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_hint) { '#js-dropdown-hint' }
@@ -14,165 +14,209 @@ describe 'Dropdown hint', :js do
before do
project.team << [user, :master]
- sign_in(user)
create(:issue, project: project)
-
- visit project_issues_path(project)
end
- describe 'behavior' do
+ context 'when user not logged in' do
before do
- expect(page).to have_css(js_dropdown_hint, visible: false)
- filtered_search.click
+ visit project_issues_path(project)
end
- it 'opens when the search bar is first focused' do
- expect(page).to have_css(js_dropdown_hint, visible: true)
- end
-
- it 'closes when the search bar is unfocused' do
- find('body').click
-
+ it 'does not exist my-reaction dropdown item' do
expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).not_to have_content('my-reaction')
end
end
- describe 'filtering' do
- it 'does not filter `Press Enter or click to search`' do
- filtered_search.set('randomtext')
-
- hint_dropdown = find(js_dropdown_hint)
+ context 'when user logged in' do
+ before do
+ sign_in(user)
- expect(hint_dropdown).to have_content('Press Enter or click to search')
- expect(hint_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 0)
+ visit project_issues_path(project)
end
- it 'filters with text' do
- filtered_search.set('a')
+ describe 'behavior' do
+ before do
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ filtered_search.click
+ end
- expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 3)
- end
- end
+ it 'opens when the search bar is first focused' do
+ expect(page).to have_css(js_dropdown_hint, visible: true)
+ end
- describe 'selecting from dropdown with no input' do
- before do
- filtered_search.click
- end
+ it 'closes when the search bar is unfocused' do
+ find('body').click
- it 'opens the author dropdown when you click on author' do
- click_hint('author')
-
- expect(page).to have_css(js_dropdown_hint, visible: false)
- expect(page).to have_css('#js-dropdown-author', visible: true)
- expect_tokens([{ name: 'author' }])
- expect_filtered_search_input_empty
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ end
end
- it 'opens the assignee dropdown when you click on assignee' do
- click_hint('assignee')
+ describe 'filtering' do
+ it 'does not filter `Press Enter or click to search`' do
+ filtered_search.set('randomtext')
- expect(page).to have_css(js_dropdown_hint, visible: false)
- expect(page).to have_css('#js-dropdown-assignee', visible: true)
- expect_tokens([{ name: 'assignee' }])
- expect_filtered_search_input_empty
- end
+ hint_dropdown = find(js_dropdown_hint)
- it 'opens the milestone dropdown when you click on milestone' do
- click_hint('milestone')
+ expect(hint_dropdown).to have_content('Press Enter or click to search')
+ expect(hint_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 0)
+ end
- expect(page).to have_css(js_dropdown_hint, visible: false)
- expect(page).to have_css('#js-dropdown-milestone', visible: true)
- expect_tokens([{ name: 'milestone' }])
- expect_filtered_search_input_empty
- end
+ it 'filters with text' do
+ filtered_search.set('a')
- it 'opens the label dropdown when you click on label' do
- click_hint('label')
-
- expect(page).to have_css(js_dropdown_hint, visible: false)
- expect(page).to have_css('#js-dropdown-label', visible: true)
- expect_tokens([{ name: 'label' }])
- expect_filtered_search_input_empty
+ expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 4)
+ end
end
- end
-
- describe 'selecting from dropdown with some input' do
- it 'opens the author dropdown when you click on author' do
- filtered_search.set('auth')
- click_hint('author')
- expect(page).to have_css(js_dropdown_hint, visible: false)
- expect(page).to have_css('#js-dropdown-author', visible: true)
- expect_tokens([{ name: 'author' }])
- expect_filtered_search_input_empty
- end
+ describe 'selecting from dropdown with no input' do
+ before do
+ filtered_search.click
+ end
- it 'opens the assignee dropdown when you click on assignee' do
- filtered_search.set('assign')
- click_hint('assignee')
+ it 'opens the author dropdown when you click on author' do
+ click_hint('author')
- expect(page).to have_css(js_dropdown_hint, visible: false)
- expect(page).to have_css('#js-dropdown-assignee', visible: true)
- expect_tokens([{ name: 'assignee' }])
- expect_filtered_search_input_empty
- end
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ expect_tokens([{ name: 'author' }])
+ expect_filtered_search_input_empty
+ end
- it 'opens the milestone dropdown when you click on milestone' do
- filtered_search.set('mile')
- click_hint('milestone')
+ it 'opens the assignee dropdown when you click on assignee' do
+ click_hint('assignee')
- expect(page).to have_css(js_dropdown_hint, visible: false)
- expect(page).to have_css('#js-dropdown-milestone', visible: true)
- expect_tokens([{ name: 'milestone' }])
- expect_filtered_search_input_empty
- end
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ expect_tokens([{ name: 'assignee' }])
+ expect_filtered_search_input_empty
+ end
- it 'opens the label dropdown when you click on label' do
- filtered_search.set('lab')
- click_hint('label')
+ it 'opens the milestone dropdown when you click on milestone' do
+ click_hint('milestone')
- expect(page).to have_css(js_dropdown_hint, visible: false)
- expect(page).to have_css('#js-dropdown-label', visible: true)
- expect_tokens([{ name: 'label' }])
- expect_filtered_search_input_empty
- end
- end
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-milestone', visible: true)
+ expect_tokens([{ name: 'milestone' }])
+ expect_filtered_search_input_empty
+ end
- describe 'reselecting from dropdown' do
- it 'reuses existing author text' do
- filtered_search.send_keys('author:')
- filtered_search.send_keys(:backspace)
- click_hint('author')
+ it 'opens the label dropdown when you click on label' do
+ click_hint('label')
- expect_tokens([{ name: 'author' }])
- expect_filtered_search_input_empty
- end
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-label', visible: true)
+ expect_tokens([{ name: 'label' }])
+ expect_filtered_search_input_empty
+ end
- it 'reuses existing assignee text' do
- filtered_search.send_keys('assignee:')
- filtered_search.send_keys(:backspace)
- click_hint('assignee')
+ it 'opens the emoji dropdown when you click on my-reaction' do
+ click_hint('my-reaction')
- expect_tokens([{ name: 'assignee' }])
- expect_filtered_search_input_empty
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-my-reaction', visible: true)
+ expect_tokens([{ name: 'my-reaction' }])
+ expect_filtered_search_input_empty
+ end
end
- it 'reuses existing milestone text' do
- filtered_search.send_keys('milestone:')
- filtered_search.send_keys(:backspace)
- click_hint('milestone')
-
- expect_tokens([{ name: 'milestone' }])
- expect_filtered_search_input_empty
- end
+ describe 'selecting from dropdown with some input' do
+ it 'opens the author dropdown when you click on author' do
+ filtered_search.set('auth')
+ click_hint('author')
- it 'reuses existing label text' do
- filtered_search.send_keys('label:')
- filtered_search.send_keys(:backspace)
- click_hint('label')
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ expect_tokens([{ name: 'author' }])
+ expect_filtered_search_input_empty
+ end
- expect_tokens([{ name: 'label' }])
- expect_filtered_search_input_empty
+ it 'opens the assignee dropdown when you click on assignee' do
+ filtered_search.set('assign')
+ click_hint('assignee')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ expect_tokens([{ name: 'assignee' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'opens the milestone dropdown when you click on milestone' do
+ filtered_search.set('mile')
+ click_hint('milestone')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-milestone', visible: true)
+ expect_tokens([{ name: 'milestone' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'opens the label dropdown when you click on label' do
+ filtered_search.set('lab')
+ click_hint('label')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-label', visible: true)
+ expect_tokens([{ name: 'label' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'opens the emoji dropdown when you click on my-reaction' do
+ filtered_search.set('my')
+ click_hint('my-reaction')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-my-reaction', visible: true)
+ expect_tokens([{ name: 'my-reaction' }])
+ expect_filtered_search_input_empty
+ end
+ end
+
+ describe 'reselecting from dropdown' do
+ it 'reuses existing author text' do
+ filtered_search.send_keys('author:')
+ filtered_search.send_keys(:backspace)
+ click_hint('author')
+
+ expect_tokens([{ name: 'author' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'reuses existing assignee text' do
+ filtered_search.send_keys('assignee:')
+ filtered_search.send_keys(:backspace)
+ click_hint('assignee')
+
+ expect_tokens([{ name: 'assignee' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'reuses existing milestone text' do
+ filtered_search.send_keys('milestone:')
+ filtered_search.send_keys(:backspace)
+ click_hint('milestone')
+
+ expect_tokens([{ name: 'milestone' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'reuses existing label text' do
+ filtered_search.send_keys('label:')
+ filtered_search.send_keys(:backspace)
+ click_hint('label')
+
+ expect_tokens([{ name: 'label' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'reuses existing emoji text' do
+ filtered_search.send_keys('my-reaction:')
+ filtered_search.send_keys(:backspace)
+ click_hint('my-reaction')
+
+ expect_tokens([{ name: 'my-reaction' }])
+ expect_filtered_search_input_empty
+ end
end
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index 67eb0ef0119..c46803112a9 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -47,7 +47,7 @@ describe 'Dropdown label', js: true do
filtered_search.native.send_keys(:down, :down, :enter)
- expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+ expect_tokens([label_token(bug_label.title)])
expect_filtered_search_input_empty
end
end
@@ -178,7 +178,7 @@ describe 'Dropdown label', js: true do
click_label(bug_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+ expect_tokens([label_token(bug_label.title)])
expect_filtered_search_input_empty
end
@@ -187,7 +187,7 @@ describe 'Dropdown label', js: true do
click_label(bug_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+ expect_tokens([label_token(bug_label.title)])
expect_filtered_search_input_empty
end
@@ -195,7 +195,7 @@ describe 'Dropdown label', js: true do
click_label(two_words_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect_tokens([{ name: 'label', value: "\"#{two_words_label.title}\"" }])
+ expect_tokens([label_token("\"#{two_words_label.title}\"")])
expect_filtered_search_input_empty
end
@@ -203,7 +203,7 @@ describe 'Dropdown label', js: true do
click_label(long_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect_tokens([{ name: 'label', value: "\"#{long_label.title}\"" }])
+ expect_tokens([label_token("\"#{long_label.title}\"")])
expect_filtered_search_input_empty
end
@@ -211,7 +211,7 @@ describe 'Dropdown label', js: true do
click_label(wont_fix_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect_tokens([{ name: 'label', value: "~'#{wont_fix_label.title}'" }])
+ expect_tokens([label_token("'#{wont_fix_label.title}'")])
expect_filtered_search_input_empty
end
@@ -219,7 +219,7 @@ describe 'Dropdown label', js: true do
click_label(uppercase_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect_tokens([{ name: 'label', value: "~#{uppercase_label.title}" }])
+ expect_tokens([label_token(uppercase_label.title)])
expect_filtered_search_input_empty
end
@@ -227,7 +227,7 @@ describe 'Dropdown label', js: true do
click_label(special_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect_tokens([{ name: 'label', value: "~#{special_label.title}" }])
+ expect_tokens([label_token(special_label.title)])
expect_filtered_search_input_empty
end
@@ -235,7 +235,7 @@ describe 'Dropdown label', js: true do
find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
expect(page).not_to have_css(js_dropdown_label)
- expect_tokens([{ name: 'label', value: 'none' }])
+ expect_tokens([label_token('none', false)])
expect_filtered_search_input_empty
end
end
@@ -270,6 +270,12 @@ describe 'Dropdown label', js: true do
expect(page).to have_css(js_dropdown_label)
end
+
+ it 'opens label dropdown with existing my-reaction' do
+ filtered_search.set('my-reaction:star label:')
+
+ expect(page).to have_css(js_dropdown_label)
+ end
end
describe 'caching requests' do
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index 456eb05f241..f6c2e952bea 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -134,7 +134,7 @@ describe 'Dropdown milestone', :js do
click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }])
+ expect_tokens([milestone_token(milestone.title)])
expect_filtered_search_input_empty
end
@@ -143,7 +143,7 @@ describe 'Dropdown milestone', :js do
click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }])
+ expect_tokens([milestone_token(milestone.title)])
expect_filtered_search_input_empty
end
@@ -151,7 +151,7 @@ describe 'Dropdown milestone', :js do
click_milestone(two_words_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([{ name: 'milestone', value: "%\"#{two_words_milestone.title}\"" }])
+ expect_tokens([milestone_token("\"#{two_words_milestone.title}\"")])
expect_filtered_search_input_empty
end
@@ -159,7 +159,7 @@ describe 'Dropdown milestone', :js do
click_milestone(long_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([{ name: 'milestone', value: "%\"#{long_milestone.title}\"" }])
+ expect_tokens([milestone_token("\"#{long_milestone.title}\"")])
expect_filtered_search_input_empty
end
@@ -167,7 +167,7 @@ describe 'Dropdown milestone', :js do
click_milestone(wont_fix_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([{ name: 'milestone', value: "%'#{wont_fix_milestone.title}'" }])
+ expect_tokens([milestone_token("'#{wont_fix_milestone.title}'")])
expect_filtered_search_input_empty
end
@@ -175,7 +175,7 @@ describe 'Dropdown milestone', :js do
click_milestone(uppercase_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([{ name: 'milestone', value: "%#{uppercase_milestone.title}" }])
+ expect_tokens([milestone_token(uppercase_milestone.title)])
expect_filtered_search_input_empty
end
@@ -183,7 +183,7 @@ describe 'Dropdown milestone', :js do
click_milestone(special_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([{ name: 'milestone', value: "%#{special_milestone.title}" }])
+ expect_tokens([milestone_token(special_milestone.title)])
expect_filtered_search_input_empty
end
@@ -191,7 +191,7 @@ describe 'Dropdown milestone', :js do
click_static_milestone('No Milestone')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([{ name: 'milestone', value: 'none' }])
+ expect_tokens([milestone_token('none', false)])
expect_filtered_search_input_empty
end
@@ -199,7 +199,7 @@ describe 'Dropdown milestone', :js do
click_static_milestone('Upcoming')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([{ name: 'milestone', value: 'upcoming' }])
+ expect_tokens([milestone_token('upcoming', false)])
expect_filtered_search_input_empty
end
@@ -207,7 +207,7 @@ describe 'Dropdown milestone', :js do
click_static_milestone('Started')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect_tokens([{ name: 'milestone', value: 'started' }])
+ expect_tokens([milestone_token('started', false)])
expect_filtered_search_input_empty
end
end
@@ -242,6 +242,12 @@ describe 'Dropdown milestone', :js do
expect(page).to have_css(js_dropdown_milestone, visible: true)
end
+
+ it 'opens milestone dropdown with existing my-reaction' do
+ filtered_search.set('my-reaction:star milestone:')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: true)
+ end
end
describe 'caching requests' do
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index cd2cbf4bfe7..3ea6e1c8863 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -1,26 +1,24 @@
require 'spec_helper'
describe 'Filter issues', js: true do
- include Devise::Test::IntegrationHelpers
include FilteredSearchHelpers
- let!(:group) { create(:group) }
- let!(:project) { create(:project, group: group) }
- let!(:user) { create(:user, username: 'joe', name: 'Joe') }
- let!(:user2) { create(:user, username: 'jane') }
- let!(:label) { create(:label, project: project) }
- let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
+ let(:project) { create(:project) }
+
+ # NOTE: The short name here is actually important
+ #
+ # When the name is longer, the filtered search input can end up scrolling
+ # horizontally, and PhantomJS can't handle it.
+ let(:user) { create(:user, name: 'Ann') }
let!(:bug_label) { create(:label, project: project, title: 'bug') }
let!(:caps_sensitive_label) { create(:label, project: project, title: 'CaPs') }
- let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
-
- let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) }
+ let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
def expect_no_issues_list
page.within '.issues-list' do
- expect(page).not_to have_selector('.issue')
+ expect(page).to have_no_selector('.issue')
end
end
@@ -33,63 +31,62 @@ describe 'Filter issues', js: true do
end
end
- def select_search_at_index(pos)
- evaluate_script("el = document.querySelector('.filtered-search'); el.focus(); el.setSelectionRange(#{pos}, #{pos});")
- end
-
before do
- project.team << [user, :master]
- project.team << [user2, :master]
- group.add_developer(user)
- group.add_developer(user2)
+ project.add_master(user)
- sign_in(user)
+ user2 = create(:user)
- create(:issue, project: project)
- create(:issue, project: project, title: "Bug report 1")
- create(:issue, project: project, title: "Bug report 2")
- create(:issue, project: project, title: "issue with 'single quotes'")
- create(:issue, project: project, title: "issue with \"double quotes\"")
- create(:issue, project: project, title: "issue with !@\#{$%^&*()-+")
- create(:issue, project: project, title: "issue by assignee", milestone: milestone, author: user, assignees: [user])
- create(:issue, project: project, title: "issue by assignee with searchTerm", milestone: milestone, author: user, assignees: [user])
+ create(:issue, project: project, author: user2, title: "Bug report 1")
+ create(:issue, project: project, author: user2, title: "Bug report 2")
- issue = create(:issue,
+ create(:issue, project: project, author: user, title: "issue by assignee", milestone: milestone, assignees: [user])
+ create(:issue, project: project, author: user, title: "issue by assignee with searchTerm", milestone: milestone, assignees: [user])
+
+ create(:labeled_issue,
title: "Bug 2",
project: project,
milestone: milestone,
author: user,
- assignees: [user])
- issue.labels << bug_label
+ assignees: [user],
+ labels: [bug_label])
- issue_with_caps_label = create(:issue,
+ create(:labeled_issue,
title: "issue by assignee with searchTerm and label",
project: project,
milestone: milestone,
author: user,
- assignees: [user])
- issue_with_caps_label.labels << caps_sensitive_label
+ assignees: [user],
+ labels: [caps_sensitive_label])
- issue_with_everything = create(:issue,
+ create(:labeled_issue,
title: "Bug report foo was possible",
project: project,
milestone: milestone,
author: user,
- assignees: [user])
- issue_with_everything.labels << bug_label
- issue_with_everything.labels << caps_sensitive_label
+ assignees: [user],
+ labels: [bug_label, caps_sensitive_label])
- multiple_words_label_issue = create(:issue, title: "Issue with multiple words label", project: project)
- multiple_words_label_issue.labels << multiple_words_label
+ create(:labeled_issue, title: "Issue with multiple words label", project: project, labels: [multiple_words_label])
- future_milestone = create(:milestone, title: "future", project: project, due_date: Time.now + 1.month)
+ sign_in(user)
+ visit project_issues_path(project)
+ end
- create(:issue,
- title: "Issue with future milestone",
- milestone: future_milestone,
- project: project)
+ it 'filters by all available tokens' do
+ search_term = 'issue'
- visit project_issues_path(project)
+ input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
+
+ wait_for_requests
+
+ expect_tokens([
+ assignee_token(user.name),
+ author_token(user.name),
+ label_token(caps_sensitive_label.title),
+ milestone_token(milestone.title)
+ ])
+ expect_issues_list_count(1)
+ expect_filtered_search_input(search_term)
end
describe 'filter issues by author' do
@@ -97,70 +94,12 @@ describe 'Filter issues', js: true do
it 'filters issues by searched author' do
input_filtered_search("author:@#{user.username}")
- expect_tokens([{ name: 'author', value: user.username }])
+ wait_for_requests
+
+ expect_tokens([author_token(user.name)])
expect_issues_list_count(5)
expect_filtered_search_input_empty
end
-
- it 'filters issues by invalid author' do
- skip('to be tested, issue #26546')
- end
-
- it 'filters issues by multiple authors' do
- skip('to be tested, issue #26546')
- end
- end
-
- context 'author with other filters' do
- let(:search_term) { 'issue' }
-
- it 'filters issues by searched author and text' do
- input_filtered_search("author:@#{user.username} #{search_term}")
-
- expect_tokens([{ name: 'author', value: user.username }])
- expect_issues_list_count(3)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched author, assignee and text' do
- input_filtered_search("author:@#{user.username} assignee:@#{user.username} #{search_term}")
-
- expect_tokens([
- { name: 'author', value: user.username },
- { name: 'assignee', value: user.username }
- ])
- expect_issues_list_count(3)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched author, assignee, label, and text' do
- input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}")
-
- expect_tokens([
- { name: 'author', value: user.username },
- { name: 'assignee', value: user.username },
- { name: 'label', value: caps_sensitive_label.title }
- ])
- expect_issues_list_count(1)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched author, assignee, label, milestone and text' do
- input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
-
- expect_tokens([
- { name: 'author', value: user.username },
- { name: 'assignee', value: user.username },
- { name: 'label', value: caps_sensitive_label.title },
- { name: 'milestone', value: milestone.title }
- ])
- expect_issues_list_count(1)
- expect_filtered_search_input(search_term)
- end
- end
-
- it 'sorting' do
- skip('to be tested, issue #26546')
end
end
@@ -169,7 +108,9 @@ describe 'Filter issues', js: true do
it 'filters issues by searched assignee' do
input_filtered_search("assignee:@#{user.username}")
- expect_tokens([{ name: 'assignee', value: user.username }])
+ wait_for_requests
+
+ expect_tokens([assignee_token(user.name)])
expect_issues_list_count(5)
expect_filtered_search_input_empty
end
@@ -177,83 +118,19 @@ describe 'Filter issues', js: true do
it 'filters issues by no assignee' do
input_filtered_search('assignee:none')
- expect_tokens([{ name: 'assignee', value: 'none' }])
- expect_issues_list_count(8, 1)
+ expect_tokens([assignee_token('none')])
+ expect_issues_list_count(3)
expect_filtered_search_input_empty
end
-
- it 'filters issues by invalid assignee' do
- skip('to be tested, issue #26546')
- end
-
- it 'filters issues by multiple assignees' do
- skip('to be tested, issue #26546')
- end
- end
-
- context 'assignee with other filters' do
- let(:search_term) { 'searchTerm' }
-
- it 'filters issues by searched assignee and text' do
- input_filtered_search("assignee:@#{user.username} #{search_term}")
-
- expect_tokens([{ name: 'assignee', value: user.username }])
- expect_issues_list_count(2)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched assignee, author and text' do
- input_filtered_search("assignee:@#{user.username} author:@#{user.username} #{search_term}")
-
- expect_tokens([
- { name: 'assignee', value: user.username },
- { name: 'author', value: user.username }
- ])
- expect_issues_list_count(2)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched assignee, author, label, text' do
- input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}")
-
- expect_tokens([
- { name: 'assignee', value: user.username },
- { name: 'author', value: user.username },
- { name: 'label', value: caps_sensitive_label.title }
- ])
- expect_issues_list_count(1)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched assignee, author, label, milestone and text' do
- input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
-
- expect_tokens([
- { name: 'assignee', value: user.username },
- { name: 'author', value: user.username },
- { name: 'label', value: caps_sensitive_label.title },
- { name: 'milestone', value: milestone.title }
- ])
- expect_issues_list_count(1)
- expect_filtered_search_input(search_term)
- end
- end
-
- context 'sorting' do
- it 'sorts' do
- skip('to be tested, issue #26546')
- end
end
end
describe 'filter issues by label' do
- let(:search_term) { 'bug' }
-
context 'only label' do
it 'filters issues by searched label' do
input_filtered_search("label:~#{bug_label.title}")
- expect_tokens([{ name: 'label', value: bug_label.title }])
+ expect_tokens([label_token(bug_label.title)])
expect_issues_list_count(2)
expect_filtered_search_input_empty
end
@@ -261,21 +138,17 @@ describe 'Filter issues', js: true do
it 'filters issues by no label' do
input_filtered_search('label:none')
- expect_tokens([{ name: 'label', value: 'none' }])
- expect_issues_list_count(9, 1)
+ expect_tokens([label_token('none', false)])
+ expect_issues_list_count(8)
expect_filtered_search_input_empty
end
- it 'filters issues by invalid label' do
- skip('to be tested, issue #26546')
- end
-
it 'filters issues by multiple labels' do
input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}")
expect_tokens([
- { name: 'label', value: bug_label.title },
- { name: 'label', value: caps_sensitive_label.title }
+ label_token(bug_label.title),
+ label_token(caps_sensitive_label.title)
])
expect_issues_list_count(1)
expect_filtered_search_input_empty
@@ -287,18 +160,19 @@ describe 'Filter issues', js: true do
special_issue.labels << special_label
input_filtered_search("label:~#{special_label.title}")
- expect_tokens([{ name: 'label', value: special_label.title }])
+
+ expect_tokens([label_token(special_label.title)])
expect_issues_list_count(1)
expect_filtered_search_input_empty
end
- it 'does not show issues' do
+ it 'does not show issues for unused labels' do
new_label = create(:label, project: project, title: 'new_label')
input_filtered_search("label:~#{new_label.title}")
- expect_tokens([{ name: 'label', value: new_label.title }])
- expect_no_issues_list()
+ expect_tokens([label_token(new_label.title)])
+ expect_no_issues_list
expect_filtered_search_input_empty
end
end
@@ -311,25 +185,27 @@ describe 'Filter issues', js: true do
input_filtered_search("label:~'#{special_multiple_label.title}'")
- # filtered search defaults quotations to double quotes
- expect_tokens([{ name: 'label', value: "\"#{special_multiple_label.title}\"" }])
+ # Check for search results (which makes sure that the page has changed)
expect_issues_list_count(1)
+ # filtered search defaults quotations to double quotes
+ expect_tokens([label_token("\"#{special_multiple_label.title}\"")])
+
expect_filtered_search_input_empty
end
it 'single quotes' do
input_filtered_search("label:~'#{multiple_words_label.title}'")
- expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
expect_issues_list_count(1)
+ expect_tokens([label_token("\"#{multiple_words_label.title}\"")])
expect_filtered_search_input_empty
end
it 'double quotes' do
input_filtered_search("label:~\"#{multiple_words_label.title}\"")
- expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
+ expect_tokens([label_token("\"#{multiple_words_label.title}\"")])
expect_issues_list_count(1)
expect_filtered_search_input_empty
end
@@ -341,7 +217,7 @@ describe 'Filter issues', js: true do
input_filtered_search("label:~'#{double_quotes_label.title}'")
- expect_tokens([{ name: 'label', value: "'#{double_quotes_label.title}'" }])
+ expect_tokens([label_token("'#{double_quotes_label.title}'")])
expect_issues_list_count(1)
expect_filtered_search_input_empty
end
@@ -353,104 +229,26 @@ describe 'Filter issues', js: true do
input_filtered_search("label:~\"#{single_quotes_label.title}\"")
- expect_tokens([{ name: 'label', value: "\"#{single_quotes_label.title}\"" }])
+ expect_tokens([label_token("\"#{single_quotes_label.title}\"")])
expect_issues_list_count(1)
expect_filtered_search_input_empty
end
end
- context 'label with other filters' do
- it 'filters issues by searched label and text' do
- input_filtered_search("label:~#{caps_sensitive_label.title} #{search_term}")
-
- expect_tokens([{ name: 'label', value: caps_sensitive_label.title }])
- expect_issues_list_count(1)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched label, author and text' do
- input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}")
-
- expect_tokens([
- { name: 'label', value: caps_sensitive_label.title },
- { name: 'author', value: user.username }
- ])
- expect_issues_list_count(1)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched label, author, assignee and text' do
- input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
-
- expect_tokens([
- { name: 'label', value: caps_sensitive_label.title },
- { name: 'author', value: user.username },
- { name: 'assignee', value: user.username }
- ])
- expect_issues_list_count(1)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched label, author, assignee, milestone and text' do
- input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}")
-
- expect_tokens([
- { name: 'label', value: caps_sensitive_label.title },
- { name: 'author', value: user.username },
- { name: 'assignee', value: user.username },
- { name: 'milestone', value: milestone.title }
- ])
- expect_issues_list_count(1)
- expect_filtered_search_input(search_term)
- end
- end
-
context 'multiple labels with other filters' do
- it 'filters issues by searched label, label2, and text' do
- input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} #{search_term}")
-
- expect_tokens([
- { name: 'label', value: bug_label.title },
- { name: 'label', value: caps_sensitive_label.title }
- ])
- expect_issues_list_count(1)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched label, label2, author and text' do
- input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}")
-
- expect_tokens([
- { name: 'label', value: bug_label.title },
- { name: 'label', value: caps_sensitive_label.title },
- { name: 'author', value: user.username }
- ])
- expect_issues_list_count(1)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched label, label2, author, assignee and text' do
- input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
-
- expect_tokens([
- { name: 'label', value: bug_label.title },
- { name: 'label', value: caps_sensitive_label.title },
- { name: 'author', value: user.username },
- { name: 'assignee', value: user.username }
- ])
- expect_issues_list_count(1)
- expect_filtered_search_input(search_term)
- end
-
it 'filters issues by searched label, label2, author, assignee, milestone and text' do
+ search_term = 'bug'
+
input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}")
+ wait_for_requests
+
expect_tokens([
- { name: 'label', value: bug_label.title },
- { name: 'label', value: caps_sensitive_label.title },
- { name: 'author', value: user.username },
- { name: 'assignee', value: user.username },
- { name: 'milestone', value: milestone.title }
+ label_token(bug_label.title),
+ label_token(caps_sensitive_label.title),
+ author_token(user.name),
+ assignee_token(user.name),
+ milestone_token(milestone.title)
])
expect_issues_list_count(1)
expect_filtered_search_input(search_term)
@@ -458,25 +256,14 @@ describe 'Filter issues', js: true do
end
context 'issue label clicked' do
- before do
+ it 'filters and displays in search bar' do
find('.issues-list .issue .issue-main-info .issuable-info a .label', text: multiple_words_label.title).click
- end
- it 'filters' do
expect_issues_list_count(1)
- end
-
- it 'displays in search bar' do
- expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
+ expect_tokens([label_token("\"#{multiple_words_label.title}\"")])
expect_filtered_search_input_empty
end
end
-
- context 'sorting' do
- it 'sorts' do
- skip('to be tested, issue #26546')
- end
- end
end
describe 'filter issues by milestone' do
@@ -484,7 +271,7 @@ describe 'Filter issues', js: true do
it 'filters issues by searched milestone' do
input_filtered_search("milestone:%#{milestone.title}")
- expect_tokens([{ name: 'milestone', value: milestone.title }])
+ expect_tokens([milestone_token(milestone.title)])
expect_issues_list_count(5)
expect_filtered_search_input_empty
end
@@ -492,15 +279,19 @@ describe 'Filter issues', js: true do
it 'filters issues by no milestone' do
input_filtered_search("milestone:none")
- expect_tokens([{ name: 'milestone', value: 'none' }])
- expect_issues_list_count(7, 1)
+ expect_tokens([milestone_token('none', false)])
+ expect_issues_list_count(3)
expect_filtered_search_input_empty
end
it 'filters issues by upcoming milestones' do
+ create(:milestone, project: project, due_date: 1.month.from_now) do |future_milestone|
+ create(:issue, project: project, milestone: future_milestone, author: user)
+ end
+
input_filtered_search("milestone:upcoming")
- expect_tokens([{ name: 'milestone', value: 'upcoming' }])
+ expect_tokens([milestone_token('upcoming', false)])
expect_issues_list_count(1)
expect_filtered_search_input_empty
end
@@ -508,94 +299,32 @@ describe 'Filter issues', js: true do
it 'filters issues by started milestones' do
input_filtered_search("milestone:started")
- expect_tokens([{ name: 'milestone', value: 'started' }])
+ expect_tokens([milestone_token('started', false)])
expect_issues_list_count(5)
expect_filtered_search_input_empty
end
- it 'filters issues by invalid milestones' do
- skip('to be tested, issue #26546')
- end
-
- it 'filters issues by multiple milestones' do
- skip('to be tested, issue #26546')
- end
-
it 'filters issues by milestone containing special characters' do
special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project)
- create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone)
+ create(:issue, project: project, milestone: special_milestone)
input_filtered_search("milestone:%#{special_milestone.title}")
- expect_tokens([{ name: 'milestone', value: special_milestone.title }])
+ expect_tokens([milestone_token(special_milestone.title)])
expect_issues_list_count(1)
expect_filtered_search_input_empty
end
- it 'does not show issues' do
- new_milestone = create(:milestone, title: "new", project: project)
+ it 'does not show issues for unused milestones' do
+ new_milestone = create(:milestone, title: 'new', project: project)
input_filtered_search("milestone:%#{new_milestone.title}")
- expect_tokens([{ name: 'milestone', value: new_milestone.title }])
- expect_no_issues_list()
+ expect_tokens([milestone_token(new_milestone.title)])
+ expect_no_issues_list
expect_filtered_search_input_empty
end
end
-
- context 'milestone with other filters' do
- let(:search_term) { 'bug' }
-
- it 'filters issues by searched milestone and text' do
- input_filtered_search("milestone:%#{milestone.title} #{search_term}")
-
- expect_tokens([{ name: 'milestone', value: milestone.title }])
- expect_issues_list_count(2)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched milestone, author and text' do
- input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} #{search_term}")
-
- expect_tokens([
- { name: 'milestone', value: milestone.title },
- { name: 'author', value: user.username }
- ])
- expect_issues_list_count(2)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched milestone, author, assignee and text' do
- input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
-
- expect_tokens([
- { name: 'milestone', value: milestone.title },
- { name: 'author', value: user.username },
- { name: 'assignee', value: user.username }
- ])
- expect_issues_list_count(2)
- expect_filtered_search_input(search_term)
- end
-
- it 'filters issues by searched milestone, author, assignee, label and text' do
- input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} #{search_term}")
-
- expect_tokens([
- { name: 'milestone', value: milestone.title },
- { name: 'author', value: user.username },
- { name: 'assignee', value: user.username },
- { name: 'label', value: bug_label.title }
- ])
- expect_issues_list_count(2)
- expect_filtered_search_input(search_term)
- end
- end
-
- context 'sorting' do
- it 'sorts' do
- skip('to be tested, issue #26546')
- end
- end
end
describe 'filter issues by text' do
@@ -604,7 +333,7 @@ describe 'Filter issues', js: true do
search = 'Bug'
input_filtered_search(search)
- expect_issues_list_count(4, 1)
+ expect_issues_list_count(4)
expect_filtered_search_input(search)
end
@@ -625,112 +354,50 @@ describe 'Filter issues', js: true do
end
it 'filters issues by searched text containing single quotes' do
- search = '\'single quotes\''
+ issue = create(:issue, project: project, author: user, title: "issue with 'single quotes'")
+
+ search = "'single quotes'"
input_filtered_search(search)
expect_issues_list_count(1)
expect_filtered_search_input(search)
+ expect(page).to have_content(issue.title)
end
it 'filters issues by searched text containing double quotes' do
+ issue = create(:issue, project: project, author: user, title: "issue with \"double quotes\"")
+
search = '"double quotes"'
input_filtered_search(search)
expect_issues_list_count(1)
expect_filtered_search_input(search)
+ expect(page).to have_content(issue.title)
end
it 'filters issues by searched text containing special characters' do
+ issue = create(:issue, project: project, author: user, title: "issue with !@\#{$%^&*()-+")
+
search = '!@#{$%^&*()-+'
input_filtered_search(search)
expect_issues_list_count(1)
expect_filtered_search_input(search)
+ expect(page).to have_content(issue.title)
end
it 'does not show any issues' do
search = 'testing'
input_filtered_search(search)
- expect_no_issues_list()
+ expect_no_issues_list
expect_filtered_search_input(search)
end
end
context 'searched text with other filters' do
- it 'filters issues by searched text and author' do
- # After searching, all search terms are placed at the end
- input_filtered_search("bug author:@#{user.username}")
-
- expect_issues_list_count(2)
- expect_filtered_search_input('bug')
- end
-
- it 'filters issues by searched text, author and more text' do
- input_filtered_search("bug author:@#{user.username} report")
-
- expect_issues_list_count(1)
- expect_filtered_search_input('bug report')
- end
-
- it 'filters issues by searched text, author and assignee' do
- input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}")
-
- expect_issues_list_count(2)
- expect_filtered_search_input('bug')
- end
-
- it 'filters issues by searched text, author, more text and assignee' do
- input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}")
-
- expect_issues_list_count(1)
- expect_filtered_search_input('bug report')
- end
-
- it 'filters issues by searched text, author, more text, assignee and even more text' do
- input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} foo")
-
- expect_issues_list_count(1)
- expect_filtered_search_input('bug report foo')
- end
-
- it 'filters issues by searched text, author, assignee and label' do
- input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}")
-
- expect_issues_list_count(2)
- expect_filtered_search_input('bug')
- end
-
- it 'filters issues by searched text, author, text, assignee, text, label and text' do
- input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} report label:~#{bug_label.title} foo")
-
- expect_issues_list_count(1)
- expect_filtered_search_input('bug report foo')
- end
-
- it 'filters issues by searched text, author, assignee, label and milestone' do
- input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}")
-
- expect_issues_list_count(2)
- expect_filtered_search_input('bug')
- end
-
- it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do
- input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} report label:~#{bug_label.title} milestone:%#{milestone.title} foo")
-
- expect_issues_list_count(1)
- expect_filtered_search_input('bug report foo')
- end
-
- it 'filters issues by searched text, author, assignee, multiple labels and milestone' do
- input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}")
-
- expect_issues_list_count(1)
- expect_filtered_search_input('bug')
- end
-
it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do
- input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} report label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} foo")
+ input_filtered_search("bug author:@#{user.username} report label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} foo")
expect_issues_list_count(1)
expect_filtered_search_input('bug report foo')
@@ -768,7 +435,9 @@ describe 'Filter issues', js: true do
end
end
- describe 'retains filter when switching issue states' do
+ describe 'switching issue states' do
+ let!(:closed_issue) { create(:issue, :closed, project: project, title: 'closed bug') }
+
before do
input_filtered_search('bug')
@@ -776,25 +445,21 @@ describe 'Filter issues', js: true do
expect_issues_list_count(4, 1)
end
- it 'open state' do
+ it 'maintains filter' do
+ # Closed
find('.issues-state-filters [data-state="closed"]').click
wait_for_requests
+ expect(page).to have_selector('.issues-list .issue', count: 1)
+ expect(page).to have_link(closed_issue.title)
+
+ # Opened
find('.issues-state-filters [data-state="opened"]').click
wait_for_requests
expect(page).to have_selector('.issues-list .issue', count: 4)
- end
- it 'closed state' do
- find('.issues-state-filters [data-state="closed"]').click
- wait_for_requests
-
- expect(page).to have_selector('.issues-list .issue', count: 1)
- expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(closed_issue.title)
- end
-
- it 'all state' do
+ # All
find('.issues-state-filters [data-state="all"]').click
wait_for_requests
@@ -803,34 +468,39 @@ describe 'Filter issues', js: true do
end
describe 'RSS feeds' do
- it 'updates atom feed link for project issues' do
- visit project_issues_path(project, milestone_title: milestone.title, assignee_id: user.id)
- link = find_link('Subscribe')
- params = CGI.parse(URI.parse(link[:href]).query)
- auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
- auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
-
- expect(params).to include('rss_token' => [user.rss_token])
- expect(params).to include('milestone_title' => [milestone.title])
- expect(params).to include('assignee_id' => [user.id.to_s])
- expect(auto_discovery_params).to include('rss_token' => [user.rss_token])
- expect(auto_discovery_params).to include('milestone_title' => [milestone.title])
- expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ shared_examples 'updates atom feed link' do |type|
+ it "for #{type}" do
+ visit path
+
+ link = find_link('Subscribe')
+ params = CGI.parse(URI.parse(link[:href]).query)
+ auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
+ auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
+
+ expected = {
+ 'rss_token' => [user.rss_token],
+ 'milestone_title' => [milestone.title],
+ 'assignee_id' => [user.id.to_s]
+ }
+
+ expect(params).to include(expected)
+ expect(auto_discovery_params).to include(expected)
+ end
+ end
+
+ it_behaves_like 'updates atom feed link', :project do
+ let(:path) { project_issues_path(project, milestone_title: milestone.title, assignee_id: user.id) }
end
- it 'updates atom feed link for group issues' do
- visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id)
- link = find('.nav-controls a', text: 'Subscribe')
- params = CGI.parse(URI.parse(link[:href]).query)
- auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
- auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
-
- expect(params).to include('rss_token' => [user.rss_token])
- expect(params).to include('milestone_title' => [milestone.title])
- expect(params).to include('assignee_id' => [user.id.to_s])
- expect(auto_discovery_params).to include('rss_token' => [user.rss_token])
- expect(auto_discovery_params).to include('milestone_title' => [milestone.title])
- expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s])
+ it_behaves_like 'updates atom feed link', :group do
+ let(:path) { issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) }
end
end
@@ -843,7 +513,7 @@ describe 'Filter issues', js: true do
input_filtered_search("milestone:", submit: false)
within('#js-dropdown-milestone') do
- expect(page).to have_selector('.filter-dropdown .filter-dropdown-item', count: 2)
+ expect(page).to have_selector('.filter-dropdown .filter-dropdown-item', count: 1)
end
end
@@ -851,7 +521,7 @@ describe 'Filter issues', js: true do
input_filtered_search("label:", submit: false)
within('#js-dropdown-label') do
- expect(page).to have_selector('.filter-dropdown .filter-dropdown-item', count: 5)
+ expect(page).to have_selector('.filter-dropdown .filter-dropdown-item', count: 3)
end
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 aa9d0d842de..d4dd570fb37 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -32,7 +32,7 @@ describe 'Search bar', js: true do
it 'selects item' do
filtered_search.native.send_keys(:down, :down, :enter)
- expect_tokens([{ name: 'author' }])
+ expect_tokens([author_token])
expect_filtered_search_input_empty
end
end
@@ -100,7 +100,7 @@ describe 'Search bar', js: true do
find('.filtered-search-box .clear-search').click
filtered_search.click
- expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: 4)
+ expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: 5)
expect(get_left_style(find('#js-dropdown-hint')['style'])).to eq(hint_offset)
end
end
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 52efe944b69..4ae54fd6f4e 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -28,6 +28,8 @@ 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)
+
visit project_issues_path(project)
end
@@ -346,8 +348,8 @@ describe 'Visual tokens', js: true do
it 'tokenizes the search term to complete visual token' do
expect_tokens([
- { name: 'author', value: '@root' },
- { name: 'assignee', value: 'none' }
+ author_token(user.name),
+ assignee_token('none')
])
end
end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 4297bfff3d9..2db6f9a2982 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -166,12 +166,10 @@ describe 'New/edit issue', :js do
end
end
- page.within '.issuable-meta' do
+ page.within '.breadcrumbs' do
issue = Issue.find_by(title: 'title')
- expect(page).to have_text("Issue #{issue.to_reference}")
- # compare paths because the host differ in test
- expect(find_link(issue.to_reference)[:href]).to end_with(issue_path(issue))
+ expect(page).to have_text("Issues #{issue.to_reference}")
end
end
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index b84635c5134..c6cf6265645 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -28,8 +28,8 @@ feature 'GFM autocomplete', js: true do
it 'opens autocomplete menu when field starts with text' do
page.within '.timeline-content-form' do
- find('#note_note').native.send_keys('')
- find('#note_note').native.send_keys('@')
+ find('#note-body').native.send_keys('')
+ find('#note-body').native.send_keys('@')
end
expect(page).to have_selector('.atwho-container')
@@ -37,8 +37,8 @@ feature 'GFM autocomplete', js: true do
it 'doesnt open autocomplete menu character is prefixed with text' do
page.within '.timeline-content-form' do
- find('#note_note').native.send_keys('testing')
- find('#note_note').native.send_keys('@')
+ find('#note-body').native.send_keys('testing')
+ find('#note-body').native.send_keys('@')
end
expect(page).not_to have_selector('.atwho-view')
@@ -46,8 +46,8 @@ feature 'GFM autocomplete', js: true do
it 'doesnt select the first item for non-assignee dropdowns' do
page.within '.timeline-content-form' do
- find('#note_note').native.send_keys('')
- find('#note_note').native.send_keys(':')
+ find('#note-body').native.send_keys('')
+ find('#note-body').native.send_keys(':')
end
expect(page).to have_selector('.atwho-container')
@@ -58,7 +58,7 @@ feature 'GFM autocomplete', js: true do
end
it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do
- note = find('#note_note')
+ note = find('#note-body')
# Number.
page.within '.timeline-content-form' do
@@ -86,8 +86,8 @@ feature 'GFM autocomplete', js: true do
it 'selects the first item for assignee dropdowns' do
page.within '.timeline-content-form' do
- find('#note_note').native.send_keys('')
- find('#note_note').native.send_keys('@')
+ find('#note-body').native.send_keys('')
+ find('#note-body').native.send_keys('@')
end
expect(page).to have_selector('.atwho-container')
@@ -99,8 +99,8 @@ feature 'GFM autocomplete', js: true do
it 'includes items for assignee dropdowns with non-ASCII characters in name' do
page.within '.timeline-content-form' do
- find('#note_note').native.send_keys('')
- find('#note_note').native.send_keys("@#{user.name[0...8]}")
+ find('#note-body').native.send_keys('')
+ find('#note-body').native.send_keys("@#{user.name[0...8]}")
end
expect(page).to have_selector('.atwho-container')
@@ -112,8 +112,8 @@ feature 'GFM autocomplete', js: true do
it 'selects the first item for non-assignee dropdowns if a query is entered' do
page.within '.timeline-content-form' do
- find('#note_note').native.send_keys('')
- find('#note_note').native.send_keys(':1')
+ find('#note-body').native.send_keys('')
+ find('#note-body').native.send_keys(':1')
end
expect(page).to have_selector('.atwho-container')
@@ -125,7 +125,7 @@ feature 'GFM autocomplete', js: true do
context 'if a selected value has special characters' do
it 'wraps the result in double quotes' do
- note = find('#note_note')
+ note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("~#{label.title[0]}")
@@ -138,7 +138,7 @@ feature 'GFM autocomplete', js: true do
end
it "shows dropdown after a new line" do
- note = find('#note_note')
+ note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys('test')
note.native.send_keys(:enter)
@@ -150,7 +150,7 @@ feature 'GFM autocomplete', js: true do
end
it "does not show dropdown when preceded with a special character" do
- note = find('#note_note')
+ note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("@")
@@ -168,7 +168,7 @@ feature 'GFM autocomplete', js: true do
end
it "does not throw an error if no labels exist" do
- note = find('#note_note')
+ note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys('~')
@@ -179,7 +179,7 @@ feature 'GFM autocomplete', js: true do
end
it 'doesn\'t wrap for assignee values' do
- note = find('#note_note')
+ note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("@#{user.username[0]}")
@@ -192,7 +192,7 @@ feature 'GFM autocomplete', js: true do
end
it 'doesn\'t wrap for emoji values' do
- note = find('#note_note')
+ note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys(":cartwheel")
@@ -206,7 +206,7 @@ feature 'GFM autocomplete', js: true do
it 'doesn\'t open autocomplete after non-word character' do
page.within '.timeline-content-form' do
- find('#note_note').native.send_keys("@#{user.username[0..2]}!")
+ find('#note-body').native.send_keys("@#{user.username[0..2]}!")
end
expect(page).not_to have_selector('.atwho-view')
@@ -214,14 +214,14 @@ feature 'GFM autocomplete', js: true do
it 'doesn\'t open autocomplete if there is no space before' do
page.within '.timeline-content-form' do
- find('#note_note').native.send_keys("hello:#{user.username[0..2]}")
+ find('#note-body').native.send_keys("hello:#{user.username[0..2]}")
end
expect(page).not_to have_selector('.atwho-view')
end
it 'triggers autocomplete after selecting a quick action' do
- note = find('#note_note')
+ note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys('/as')
diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb
index 8c23fcd483b..634ea111dc1 100644
--- a/spec/features/issues/markdown_toolbar_spec.rb
+++ b/spec/features/issues/markdown_toolbar_spec.rb
@@ -12,26 +12,26 @@ feature 'Issue markdown toolbar', js: true do
end
it "doesn't include first new line when adding bold" do
- find('#note_note').native.send_keys('test')
- find('#note_note').native.send_key(:enter)
- find('#note_note').native.send_keys('bold')
+ find('#note-body').native.send_keys('test')
+ find('#note-body').native.send_key(:enter)
+ find('#note-body').native.send_keys('bold')
- page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 9)')
+ page.evaluate_script('document.querySelectorAll(".js-main-target-form #note-body")[0].setSelectionRange(4, 9)')
first('.toolbar-btn').click
- expect(find('#note_note')[:value]).to eq("test\n**bold**\n")
+ expect(find('#note-body')[:value]).to eq("test\n**bold**\n")
end
it "doesn't include first new line when adding underline" do
- find('#note_note').native.send_keys('test')
- find('#note_note').native.send_key(:enter)
- find('#note_note').native.send_keys('underline')
+ find('#note-body').native.send_keys('test')
+ find('#note-body').native.send_key(:enter)
+ find('#note-body').native.send_keys('underline')
- page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 50)')
+ page.evaluate_script('document.querySelectorAll(".js-main-target-form #note-body")[0].setSelectionRange(4, 50)')
find('.toolbar-btn:nth-child(2)').click
- expect(find('#note_note')[:value]).to eq("test\n*underline*\n")
+ expect(find('#note-body')[:value]).to eq("test\n*underline*\n")
end
end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 494c309c9ea..b2724945da4 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -15,11 +15,11 @@ feature 'issue move to another project' do
background do
old_project.team << [user, :guest]
- edit_issue(issue)
+ visit issue_path(issue)
end
scenario 'moving issue to another project not allowed' do
- expect(page).to have_no_selector('#move_to_project_id')
+ expect(page).to have_no_selector('.js-sidebar-move-issue-block')
end
end
@@ -34,12 +34,14 @@ feature 'issue move to another project' do
old_project.team << [user, :reporter]
new_project.team << [user, :reporter]
- edit_issue(issue)
+ visit issue_path(issue)
end
scenario 'moving issue to another project', js: true do
- find('#issuable-move', visible: false).set(new_project.id)
- click_button('Save changes')
+ find('.js-move-issue').trigger('click')
+ wait_for_requests
+ all('.js-move-issue-dropdown-item')[0].click
+ find('.js-move-issue-confirmation-button').click
expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}")
expect(page).to have_content("moved from #{cross_reference}#{issue.to_reference}")
@@ -50,13 +52,12 @@ feature 'issue move to another project' do
scenario 'searching project dropdown', js: true do
new_project_search.team << [user, :reporter]
- page.within '.detail-page-description' do
- first('.select2-choice').click
- end
+ find('.js-move-issue').trigger('click')
+ wait_for_requests
- fill_in('s2id_autogen1_search', with: new_project_search.name)
+ page.within '.js-sidebar-move-issue-block' do
+ fill_in('sidebar-move-issue-dropdown-search', with: new_project_search.name)
- page.within '.select2-drop' do
expect(page).to have_content(new_project_search.name)
expect(page).not_to have_content(new_project.name)
end
@@ -68,10 +69,10 @@ feature 'issue move to another project' do
background { another_project.team << [user, :guest] }
scenario 'browsing projects in projects select' do
- click_link 'Move to a different project'
+ find('.js-move-issue').trigger('click')
+ wait_for_requests
- page.within '.select2-results' do
- expect(page).to have_content 'No project'
+ page.within '.js-sidebar-move-issue-block' do
expect(page).to have_content new_project.name_with_namespace
end
end
@@ -89,11 +90,6 @@ feature 'issue move to another project' do
end
end
- def edit_issue(issue)
- visit issue_path(issue)
- page.within('.issuable-actions') { first(:link, 'Edit').click }
- end
-
def issue_path(issue)
project_issue_path(issue.project, issue)
end
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
index 9f08ecc214b..793572851da 100644
--- a/spec/features/issues/note_polling_spec.rb
+++ b/spec/features/issues/note_polling_spec.rb
@@ -13,7 +13,7 @@ feature 'Issue notes polling', :js do
it 'displays the new comment' do
note = create(:note, noteable: issue, project: project, note: 'Looks good!')
- page.execute_script('notes.refresh();')
+ wait_for_requests
expect(page).to have_selector("#note_#{note.id}", text: 'Looks good!')
end
@@ -31,16 +31,6 @@ feature 'Issue notes polling', :js do
visit project_issue_path(project, issue)
end
- it 'has .original-note-content to compare against' do
- expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
- expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false)
-
- update_note(existing_note, updated_text)
-
- expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
- expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false)
- end
-
it 'displays the updated content' do
expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
@@ -49,24 +39,14 @@ feature 'Issue notes polling', :js do
expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
end
- it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do
- click_edit_action(existing_note)
-
- expect(page).to have_field("note[note]", with: note_text)
-
- update_note(existing_note, updated_text)
-
- expect(page).to have_field("note[note]", with: updated_text)
- end
-
- it 'when editing but you changed some things, and an update comes in, show a warning' do
+ it 'when editing but have not changed anything, and an update comes in, show warning and does not update the note' do
click_edit_action(existing_note)
expect(page).to have_field("note[note]", with: note_text)
- find("#note_#{existing_note.id} .js-note-text").set('something random')
update_note(existing_note, updated_text)
+ expect(page).not_to have_field("note[note]", with: updated_text)
expect(page).to have_selector(".alert")
end
@@ -75,8 +55,6 @@ feature 'Issue notes polling', :js do
expect(page).to have_field("note[note]", with: note_text)
- find("#note_#{existing_note.id} .js-note-text").set('something random')
-
update_note(existing_note, updated_text)
find("#note_#{existing_note.id} .note-edit-cancel").click
@@ -97,14 +75,12 @@ feature 'Issue notes polling', :js do
visit project_issue_path(project, issue)
end
- it 'has .original-note-content to compare against' do
+ it 'displays the updated content' do
expect(page).to have_selector("#note_#{existing_note.id}", text: note_text)
- expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false)
update_note(existing_note, updated_text)
expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text)
- expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false)
end
end
@@ -118,23 +94,20 @@ feature 'Issue notes polling', :js do
visit project_issue_path(project, issue)
end
- it 'has .original-note-content to compare against' do
+ it 'shows the system note' do
expect(page).to have_selector("#note_#{system_note.id}", text: note_text)
- expect(page).to have_selector("#note_#{system_note.id} .original-note-content", count: 1, visible: false)
end
end
end
def update_note(note, new_text)
note.update(note: new_text)
- page.execute_script('notes.refresh();')
+ wait_for_requests
end
def click_edit_action(note)
note_element = find("#note_#{note.id}")
- open_more_actions_dropdown(note)
-
note_element.find('.js-note-edit').click
end
end
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index 5a7c4f54cb6..bcc6e9bab0f 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -14,7 +14,7 @@ feature 'Multiple issue updating from issues#index', :js do
it 'sets to closed' do
visit project_issues_path(project)
- click_button 'Edit Issues'
+ click_button 'Edit issues'
find('#check-all-issues').click
find('.js-issue-status').click
@@ -27,7 +27,7 @@ feature 'Multiple issue updating from issues#index', :js do
create_closed
visit project_issues_path(project, state: 'closed')
- click_button 'Edit Issues'
+ click_button 'Edit issues'
find('#check-all-issues').click
find('.js-issue-status').click
@@ -41,7 +41,7 @@ feature 'Multiple issue updating from issues#index', :js do
it 'updates to current user' do
visit project_issues_path(project)
- click_button 'Edit Issues'
+ click_button 'Edit issues'
find('#check-all-issues').click
click_update_assignee_button
@@ -57,7 +57,7 @@ feature 'Multiple issue updating from issues#index', :js do
create_assigned
visit project_issues_path(project)
- click_button 'Edit Issues'
+ click_button 'Edit issues'
find('#check-all-issues').click
click_update_assignee_button
@@ -73,7 +73,7 @@ feature 'Multiple issue updating from issues#index', :js do
it 'updates milestone' do
visit project_issues_path(project)
- click_button 'Edit Issues'
+ click_button 'Edit issues'
find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
@@ -89,7 +89,7 @@ feature 'Multiple issue updating from issues#index', :js do
expect(first('.issue')).to have_content milestone.title
- click_button 'Edit Issues'
+ click_button 'Edit issues'
find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 4b63cc844f3..9261acda9dc 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -155,5 +155,114 @@ feature 'Issues > User uses quick actions', js: true do
end
end
end
+
+ describe 'move the issue to another project' do
+ let(:issue) { create(:issue, project: project) }
+
+ context 'when the project is valid', js: true do
+ let(:target_project) { create(:project, :public) }
+
+ before do
+ target_project.team << [user, :master]
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'moves the issue' do
+ write_note("/move #{target_project.full_path}")
+
+ expect(page).to have_content 'Commands applied'
+ expect(issue.reload).to be_closed
+
+ visit project_issue_path(target_project, issue)
+
+ expect(page).to have_content 'Issues 1'
+ end
+ end
+
+ context 'when the project is valid but the user not authorized', js: true do
+ let(:project_unauthorized) {create(:project, :public)}
+
+ before do
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'does not move the issue' do
+ write_note("/move #{project_unauthorized.full_path}")
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(issue.reload).to be_open
+ end
+ end
+
+ context 'when the project is invalid', js: true do
+ before do
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'does not move the issue' do
+ write_note("/move not/valid")
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(issue.reload).to be_open
+ end
+ end
+
+ context 'when the user issues multiple commands', js: true 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) }
+ let(:bug) { create(:label, project: project, title: 'bug') }
+ let(:wontfix) { create(:label, project: project, title: 'wontfix') }
+ let(:bug_target) { create(:label, project: target_project, title: 'bug') }
+ let(:wontfix_target) { create(:label, project: target_project, title: 'wontfix') }
+
+ before do
+ target_project.team << [user, :master]
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'applies the commands to both issues and moves the issue' do
+ write_note("/label ~#{bug.title} ~#{wontfix.title}\n/milestone %\"#{milestone.title}\"\n/move #{target_project.full_path}")
+
+ expect(page).to have_content 'Commands applied'
+ expect(issue.reload).to be_closed
+
+ visit project_issue_path(target_project, issue)
+
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content '1.0'
+
+ visit project_issue_path(project, issue)
+ expect(page).to have_content 'Closed'
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content '1.0'
+ end
+
+ it 'moves the issue and applies the commands to both issues' do
+ write_note("/move #{target_project.full_path}\n/label ~#{bug.title} ~#{wontfix.title}\n/milestone %\"#{milestone.title}\"")
+
+ expect(page).to have_content 'Commands applied'
+ expect(issue.reload).to be_closed
+
+ visit project_issue_path(target_project, issue)
+
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content '1.0'
+
+ visit project_issue_path(project, issue)
+ expect(page).to have_content 'Closed'
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content '1.0'
+ end
+ end
+ end
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index a5bb642221c..5c284a1fe5f 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -271,17 +271,21 @@ describe 'Issues' do
it 'filters by none' do
visit project_issues_path(project, due_date: Issue::NoDueDate.name)
- expect(page).not_to have_content('foo')
- expect(page).not_to have_content('bar')
- expect(page).to have_content('baz')
+ page.within '.issues-holder' do
+ expect(page).not_to have_content('foo')
+ expect(page).not_to have_content('bar')
+ expect(page).to have_content('baz')
+ end
end
it 'filters by any' do
visit project_issues_path(project, due_date: Issue::AnyDueDate.name)
- expect(page).to have_content('foo')
- expect(page).to have_content('bar')
- expect(page).to have_content('baz')
+ page.within '.issues-holder' do
+ expect(page).to have_content('foo')
+ expect(page).to have_content('bar')
+ expect(page).to have_content('baz')
+ end
end
it 'filters by due this week' do
@@ -291,9 +295,11 @@ describe 'Issues' do
visit project_issues_path(project, due_date: Issue::DueThisWeek.name)
- expect(page).to have_content('foo')
- expect(page).to have_content('bar')
- expect(page).not_to have_content('baz')
+ page.within '.issues-holder' do
+ expect(page).to have_content('foo')
+ expect(page).to have_content('bar')
+ expect(page).not_to have_content('baz')
+ end
end
it 'filters by due this month' do
@@ -303,9 +309,11 @@ describe 'Issues' do
visit project_issues_path(project, due_date: Issue::DueThisMonth.name)
- expect(page).to have_content('foo')
- expect(page).to have_content('bar')
- expect(page).not_to have_content('baz')
+ page.within '.issues-holder' do
+ expect(page).to have_content('foo')
+ expect(page).to have_content('bar')
+ expect(page).not_to have_content('baz')
+ end
end
it 'filters by overdue' do
@@ -315,9 +323,11 @@ describe 'Issues' do
visit project_issues_path(project, due_date: Issue::Overdue.name)
- expect(page).not_to have_content('foo')
- expect(page).not_to have_content('bar')
- expect(page).to have_content('baz')
+ page.within '.issues-holder' do
+ expect(page).not_to have_content('foo')
+ expect(page).not_to have_content('bar')
+ expect(page).to have_content('baz')
+ end
end
end
@@ -567,7 +577,9 @@ describe 'Issues' do
it 'redirects to signin then back to new issue after signin' do
visit project_issues_path(project)
- click_link 'New issue'
+ page.within '.nav-controls' do
+ click_link 'New issue'
+ end
expect(current_path).to eq new_user_session_path
@@ -708,7 +720,7 @@ describe 'Issues' do
end
describe 'confidential issue#show', js: true do
- it 'shows confidential sibebar information as confidential and can be turned off' 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)
@@ -729,7 +741,6 @@ describe 'Issues' do
visit project_issue_path(project, issue)
expect(page).not_to have_css('.is-confidential')
- expect(page).to have_css('.is-not-confidential')
end
end
end
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 2c560632a1b..2d2c674f8fb 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -28,11 +28,12 @@ feature 'Merge request conflict resolution', js: true do
end
click_button 'Commit conflict resolution'
- wait_for_requests
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
+ wait_for_requests
+
click_on 'Changes'
wait_for_requests
@@ -69,10 +70,12 @@ feature 'Merge request conflict resolution', js: true do
end
click_button 'Commit conflict resolution'
- wait_for_requests
+
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
+ wait_for_requests
+
click_on 'Changes'
wait_for_requests
@@ -140,12 +143,13 @@ feature 'Merge request conflict resolution', js: true do
end
click_button 'Commit conflict resolution'
- wait_for_requests
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
+ wait_for_requests
+
click_on 'Changes'
wait_for_requests
click_link 'Expand all'
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index d7f3d91e625..96e8027a54d 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -13,7 +13,9 @@ feature 'Create New Merge Request', js: true do
it 'selects the source branch sha when a tag with the same name exists' do
visit project_merge_requests_path(project)
- click_link 'New merge request'
+ page.within '.content' do
+ click_link 'New merge request'
+ end
expect(page).to have_content('Source branch')
expect(page).to have_content('Target branch')
@@ -26,7 +28,9 @@ feature 'Create New Merge Request', js: true do
it 'selects the target branch sha when a tag with the same name exists' do
visit project_merge_requests_path(project)
- click_link 'New merge request'
+ page.within '.content' do
+ click_link 'New merge request'
+ end
expect(page).to have_content('Source branch')
expect(page).to have_content('Target branch')
@@ -40,7 +44,9 @@ feature 'Create New Merge Request', js: true do
it 'generates a diff for an orphaned branch' do
visit project_merge_requests_path(project)
- page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
+ page.within '.content' do
+ click_link 'New merge request'
+ end
expect(page).to have_content('Source branch')
expect(page).to have_content('Target branch')
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
index 2d9419d6124..ca536f2800c 100644
--- a/spec/features/merge_requests/diff_notes_avatars_spec.rb
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -21,6 +21,8 @@ feature 'Diff note avatars', js: true do
before do
project.team << [user, :master]
sign_in user
+
+ allow_any_instance_of(ApplicationHelper).to receive(:collapsed_sidebar?).and_return(true)
end
context 'discussion tab' do
@@ -137,7 +139,7 @@ feature 'Diff note avatars', js: true do
end
page.within find("[id='#{position.line_code(project.repository)}']") do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').trigger('click')
expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
end
@@ -150,14 +152,14 @@ feature 'Diff note avatars', js: true do
page.within '.js-discussion-note-form' do
find('.js-note-text').native.send_keys('Test')
- find('.js-comment-button').trigger 'click'
+ find('.js-comment-button').trigger('click')
wait_for_requests
end
end
page.within find("[id='#{position.line_code(project.repository)}']") do
- find('.diff-notes-collapse').click
+ find('.diff-notes-collapse').trigger('click')
expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
expect(find('.diff-comments-more-count')).to have_content '+1'
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index ac7f75bd308..fd110e68e84 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -196,10 +196,11 @@ feature 'Diff notes resolve', js: true do
end
it 'does not mark discussion as resolved when resolving single note' do
- page.first '.diff-content .note' do
+ page.within("#note_#{note.id}") do
first('.line-resolve-btn').click
- expect(page).to have_selector('.note-action-button .loading')
+ wait_for_requests
+
expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
end
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
index 201be4b9e40..e9068f722d5 100644
--- a/spec/features/merge_requests/diffs_spec.rb
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -5,7 +5,7 @@ feature 'Diffs URL', js: true do
let(:merge_request) { create(:merge_request, source_project: project) }
context 'when visit with */* as accept header' do
- before(:each) do
+ before do
page.driver.add_header('Accept', '*/*')
end
@@ -88,7 +88,7 @@ feature 'Diffs URL', js: true do
visit diffs_project_merge_request_path(project, merge_request)
# Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax
- find("[id=\"#{changelog_id}\"] .js-edit-blob").click
+ find("[id=\"#{changelog_id}\"] .js-edit-blob").trigger('click')
expect(page).to have_selector('.js-fork-suggestion-button', count: 1)
expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1)
diff --git a/spec/features/merge_requests/discussion_spec.rb b/spec/features/merge_requests/discussion_spec.rb
index d1cc43e0690..05789bbd31d 100644
--- a/spec/features/merge_requests/discussion_spec.rb
+++ b/spec/features/merge_requests/discussion_spec.rb
@@ -26,7 +26,7 @@ feature 'Merge Request Discussions' do
let(:outdated_diff_refs) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e").diff_refs }
- before(:each) do
+ before do
visit project_merge_request_path(project, merge_request)
end
@@ -71,7 +71,7 @@ feature 'Merge Request Discussions' do
end
end
- before(:each) do
+ before do
visit project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/merge_requests/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb
index 1d52a4659ad..9912e8165e6 100644
--- a/spec/features/merge_requests/filter_by_labels_spec.rb
+++ b/spec/features/merge_requests/filter_by_labels_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Merge Request filtering by Labels', js: true do
+feature 'Merge Request filtering by Labels', :js do
include FilteredSearchHelpers
include MergeRequestHelpers
@@ -12,9 +12,9 @@ feature 'Merge Request filtering by Labels', js: true do
let!(:feature) { create(:label, project: project, title: 'feature') }
let!(:enhancement) { create(:label, project: project, title: 'enhancement') }
- let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") }
- let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") }
- let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") }
+ let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "fix") }
+ let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "wip") }
+ let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "improve/awesome") }
before do
mr1.labels << bug
@@ -25,7 +25,7 @@ feature 'Merge Request filtering by Labels', js: true do
mr3.title = "Feature1"
mr3.labels << feature
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
visit project_merge_requests_path(project)
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index 521fcabc881..166c02a7a7f 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -25,7 +25,7 @@ feature 'Merge Request filtering by Milestone' do
visit_merge_requests(project)
input_filtered_search('milestone:none')
- expect_tokens([{ name: 'milestone', value: 'none' }])
+ expect_tokens([milestone_token('none', false)])
expect_filtered_search_input_empty
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
index f0019be86ad..b51ae0890e4 100644
--- a/spec/features/merge_requests/filter_merge_requests_spec.rb
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -12,7 +12,7 @@ describe 'Filter merge requests' do
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
before do
- project.team << [user, :master]
+ project.add_master(user)
group.add_developer(user)
sign_in(user)
create(:merge_request, source_project: project, target_project: project)
@@ -24,7 +24,9 @@ describe 'Filter merge requests' do
let(:search_query) { "assignee:@#{user.username}" }
def expect_assignee_visual_tokens
- expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ wait_for_requests
+
+ expect_tokens([assignee_token(user.name)])
expect_filtered_search_input_empty
end
@@ -57,7 +59,7 @@ describe 'Filter merge requests' do
let(:search_query) { "milestone:%\"#{milestone.title}\"" }
def expect_milestone_visual_tokens
- expect_tokens([{ name: 'milestone', value: "%\"#{milestone.title}\"" }])
+ expect_tokens([milestone_token("\"#{milestone.title}\"")])
expect_filtered_search_input_empty
end
@@ -91,7 +93,7 @@ describe 'Filter merge requests' do
input_filtered_search('label:none')
expect_mr_list_count(1)
- expect_tokens([{ name: 'label', value: 'none' }])
+ expect_tokens([label_token('none', false)])
expect_filtered_search_input_empty
end
@@ -99,7 +101,7 @@ describe 'Filter merge requests' do
input_filtered_search("label:~#{label.title}")
expect_mr_list_count(0)
- expect_tokens([{ name: 'label', value: "~#{label.title}" }])
+ expect_tokens([label_token(label.title)])
expect_filtered_search_input_empty
end
@@ -107,10 +109,7 @@ describe 'Filter merge requests' do
input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}")
expect_mr_list_count(0)
- expect_tokens([
- { name: 'label', value: "~\"#{wontfix.title}\"" },
- { name: 'label', value: "~#{label.title}" }
- ])
+ expect_tokens([label_token("\"#{wontfix.title}\""), label_token(label.title)])
expect_filtered_search_input_empty
end
@@ -118,16 +117,13 @@ describe 'Filter merge requests' do
input_filtered_search("label:~\"#{wontfix.title}\"")
expect_mr_list_count(0)
- expect_tokens([{ name: 'label', value: "~\"#{wontfix.title}\"" }])
+ expect_tokens([label_token("\"#{wontfix.title}\"")])
expect_filtered_search_input_empty
input_filtered_search_keys("label:~#{label.title}")
expect_mr_list_count(0)
- expect_tokens([
- { name: 'label', value: "~\"#{wontfix.title}\"" },
- { name: 'label', value: "~#{label.title}" }
- ])
+ expect_tokens([label_token("\"#{wontfix.title}\""), label_token(label.title)])
expect_filtered_search_input_empty
end
end
@@ -143,10 +139,9 @@ describe 'Filter merge requests' do
context 'assignee and label', js: true do
def expect_assignee_label_visual_tokens
- expect_tokens([
- { name: 'assignee', value: "@#{user.username}" },
- { name: 'label', value: "~#{label.title}" }
- ])
+ wait_for_requests
+
+ expect_tokens([assignee_token(user.name), label_token(label.title)])
expect_filtered_search_input_empty
end
@@ -170,7 +165,7 @@ describe 'Filter merge requests' do
describe 'filter merge requests by text' do
before do
- create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "bug")
+ create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "wip")
bug_label = create(:label, project: project, title: 'bug')
milestone = create(:milestone, title: "8", project: project)
@@ -179,7 +174,7 @@ describe 'Filter merge requests' do
title: "Bug 2",
source_project: project,
target_project: project,
- source_branch: "bug2",
+ source_branch: "fix",
milestone: milestone,
author: user,
assignee: user)
@@ -214,7 +209,7 @@ describe 'Filter merge requests' do
input_filtered_search_keys(' label:~bug')
expect_mr_list_count(1)
- expect_tokens([{ name: 'label', value: '~bug' }])
+ expect_tokens([label_token('bug')])
expect_filtered_search_input('Bug')
end
@@ -227,7 +222,7 @@ describe 'Filter merge requests' do
input_filtered_search_keys(' milestone:%8')
expect_mr_list_count(1)
- expect_tokens([{ name: 'milestone', value: '%8' }])
+ expect_tokens([milestone_token('8')])
expect_filtered_search_input('Bug')
end
@@ -240,7 +235,10 @@ describe 'Filter merge requests' do
input_filtered_search_keys(" assignee:@#{user.username}")
expect_mr_list_count(1)
- expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+
+ wait_for_requests
+
+ expect_tokens([assignee_token(user.name)])
expect_filtered_search_input('Bug')
end
@@ -252,19 +250,21 @@ describe 'Filter merge requests' do
input_filtered_search_keys(" author:@#{user.username}")
+ wait_for_requests
+
expect_mr_list_count(1)
- expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_tokens([author_token(user.name)])
expect_filtered_search_input('Bug')
end
end
end
- describe 'filter merge requests and sort', js: true do
+ describe 'filter merge requests and sort', :js do
before do
bug_label = create(:label, project: project, title: 'bug')
- mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "Frontend")
- mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "bug2")
+ mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "wip")
+ mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "fix")
mr1.labels << bug_label
mr2.labels << bug_label
@@ -293,7 +293,9 @@ describe 'Filter merge requests' do
it 'filter by current user' do
visit project_merge_requests_path(project, assignee_id: user.id)
- expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ wait_for_requests
+
+ expect_tokens([assignee_token(user.name)])
expect_filtered_search_input_empty
end
@@ -303,7 +305,9 @@ describe 'Filter merge requests' do
visit project_merge_requests_path(project, assignee_id: new_user.id)
- expect_tokens([{ name: 'assignee', value: "@#{new_user.username}" }])
+ wait_for_requests
+
+ expect_tokens([assignee_token(new_user.name)])
expect_filtered_search_input_empty
end
end
@@ -312,7 +316,9 @@ describe 'Filter merge requests' do
it 'filter by current user' do
visit project_merge_requests_path(project, author_id: user.id)
- expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ wait_for_requests
+
+ expect_tokens([author_token(user.name)])
expect_filtered_search_input_empty
end
@@ -322,7 +328,9 @@ describe 'Filter merge requests' do
visit project_merge_requests_path(project, author_id: new_user.id)
- expect_tokens([{ name: 'author', value: "@#{new_user.username}" }])
+ wait_for_requests
+
+ expect_tokens([author_token(new_user.name)])
expect_filtered_search_input_empty
end
end
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
index 89410b0e90f..de98b147d04 100644
--- a/spec/features/merge_requests/form_spec.rb
+++ b/spec/features/merge_requests/form_spec.rb
@@ -84,13 +84,10 @@ describe 'New/edit merge request', :js do
end
end
- page.within '.issuable-meta' do
+ page.within '.breadcrumbs' do
merge_request = MergeRequest.find_by(source_branch: 'fix')
- expect(page).to have_text("Merge request #{merge_request.to_reference}")
- # compare paths because the host differ in test
- expect(find_link(merge_request.to_reference)[:href])
- .to end_with(merge_request_path(merge_request))
+ expect(page).to have_text("Merge Requests #{merge_request.to_reference}")
end
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 429bc277d73..08a3bb84aac 100644
--- a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
+++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
@@ -19,7 +19,7 @@ feature 'Clicking toggle commit message link', js: true do
"Merge branch 'feature' into 'master'",
merge_request.title,
"Closes #{issue_1.to_reference} and #{issue_2.to_reference}",
- "See merge request #{merge_request.to_reference}"
+ "See merge request #{merge_request.to_reference(full: true)}"
].join("\n\n")
end
let(:message_with_description) do
@@ -27,7 +27,7 @@ feature 'Clicking toggle commit message link', js: true do
"Merge branch 'feature' into 'master'",
merge_request.title,
merge_request.description,
- "See merge request #{merge_request.to_reference}"
+ "See merge request #{merge_request.to_reference(full: true)}"
].join("\n\n")
end
diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
index b1215f9ba63..dcc70338d7f 100644
--- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -70,7 +70,7 @@ feature 'Mini Pipeline Graph', :js do
it 'should show tooltip when hovered' do
toggle.hover
- expect(toggle.find(:xpath, '..')).to have_selector('.tooltip')
+ expect(page).to have_selector('.tooltip')
end
end
@@ -117,7 +117,7 @@ feature 'Mini Pipeline Graph', :js do
it 'should show tooltip when hovered' do
build_item.hover
- expect(build_item.find(:xpath, '..')).to have_selector('.tooltip')
+ expect(page).to have_selector('.tooltip')
end
end
end
diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
index c1b90e5f875..eed95816bdf 100644
--- a/spec/features/merge_requests/reset_filters_spec.rb
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Merge requests filter clear button', js: true do
+feature 'Merge requests filter clear button', :js do
include FilteredSearchHelpers
include MergeRequestHelpers
include IssueHelpers
@@ -9,8 +9,8 @@ feature 'Merge requests filter clear button', js: true do
let!(:user) { create(:user) }
let!(:milestone) { create(:milestone, project: project) }
let!(:bug) { create(:label, project: project, name: 'bug')}
- let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) }
- let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") }
+ let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "improve/awesome", milestone: milestone, author: user, assignee: user) }
+ let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "fix") }
let(:merge_request_css) { '.merge-request' }
let(:clear_search_css) { '.filtered-search-box .clear-search' }
diff --git a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb
new file mode 100644
index 00000000000..55a82bdf2b9
--- /dev/null
+++ b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb
@@ -0,0 +1,78 @@
+require 'spec_helper'
+
+feature 'Resolve outdated diff discussions', js: true do
+ let(:project) { create(:project, :repository, :public) }
+
+ let(:merge_request) do
+ create(:merge_request, source_project: project, source_branch: 'csv', target_branch: 'master')
+ end
+
+ let(:outdated_diff_refs) { project.commit('926c6595b263b2a40da6b17f3e3b7ea08344fad6').diff_refs }
+ let(:current_diff_refs) { merge_request.diff_refs }
+
+ let(:outdated_position) do
+ Gitlab::Diff::Position.new(
+ old_path: 'files/csv/Book1.csv',
+ new_path: 'files/csv/Book1.csv',
+ old_line: nil,
+ new_line: 9,
+ diff_refs: outdated_diff_refs
+ )
+ end
+
+ let(:current_position) do
+ Gitlab::Diff::Position.new(
+ old_path: 'files/csv/Book1.csv',
+ new_path: 'files/csv/Book1.csv',
+ old_line: nil,
+ new_line: 1,
+ diff_refs: current_diff_refs
+ )
+ end
+
+ let!(:outdated_discussion) do
+ create(:diff_note_on_merge_request,
+ project: project,
+ noteable: merge_request,
+ position: outdated_position).to_discussion
+ end
+
+ let!(:current_discussion) do
+ create(:diff_note_on_merge_request,
+ noteable: merge_request,
+ project: project,
+ position: current_position).to_discussion
+ end
+
+ before do
+ sign_in(merge_request.author)
+ end
+
+ context 'when a discussion was resolved by a push' do
+ before do
+ project.update!(resolve_outdated_diff_discussions: true)
+
+ merge_request.update_diff_discussion_positions(
+ old_diff_refs: outdated_diff_refs,
+ new_diff_refs: current_diff_refs,
+ current_user: merge_request.author
+ )
+
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'shows that as automatically resolved' do
+ within(".discussion[data-discussion-id='#{outdated_discussion.id}']") do
+ expect(page).to have_css('.discussion-body', visible: false)
+ expect(page).to have_content('Automatically resolved')
+ end
+ end
+
+ it 'does not show that for active discussions' do
+ within(".discussion[data-discussion-id='#{current_discussion.id}']") do
+ expect(page).to have_css('.discussion-body', visible: true)
+ expect(page).not_to have_content('Automatically resolved')
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb
index cf30a687df8..e6dc284cba7 100644
--- a/spec/features/merge_requests/update_merge_requests_spec.rb
+++ b/spec/features/merge_requests/update_merge_requests_spec.rb
@@ -98,7 +98,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
def change_status(text)
- click_button 'Edit Merge Requests'
+ click_button 'Edit merge requests'
find('#check-all-issues').click
find('.js-issue-status').click
find('.dropdown-menu-status a', text: text).click
@@ -106,7 +106,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
def change_assignee(text)
- click_button 'Edit Merge Requests'
+ click_button 'Edit merge requests'
find('#check-all-issues').click
find('.js-update-assignee').click
wait_for_requests
@@ -119,7 +119,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do
end
def change_milestone(text)
- click_button 'Edit Merge Requests'
+ click_button 'Edit merge requests'
find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: text).click
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 d62b035b40b..20008b4e7f9 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -24,12 +24,10 @@ describe 'Projects > Merge requests > User lists merge requests' do
milestone: create(:milestone, due_date: '2013-12-12'),
created_at: 2.minutes.ago,
updated_at: 2.minutes.ago)
- # lfs in itself is not a great choice for the title if one wants to match the whole body content later on
- # just think about the scenario when faker generates 'Chester Runolfsson' as the user's name
create(:merge_request,
- title: 'merge_lfs',
+ title: 'merge-test',
source_project: project,
- source_branch: 'merge_lfs',
+ source_branch: 'merge-test',
created_at: 3.minutes.ago,
updated_at: 10.seconds.ago)
end
@@ -38,7 +36,7 @@ describe 'Projects > Merge requests > User lists merge requests' do
visit_merge_requests(project, assignee_id: IssuableFinder::NONE)
expect(current_path).to eq(project_merge_requests_path(project))
- expect(page).to have_content 'merge_lfs'
+ expect(page).to have_content 'merge-test'
expect(page).not_to have_content 'fix'
expect(page).not_to have_content 'markdown'
expect(count_merge_requests).to eq(1)
@@ -47,7 +45,7 @@ describe 'Projects > Merge requests > User lists merge requests' do
it 'filters on a specific assignee' do
visit_merge_requests(project, assignee_id: user.id)
- expect(page).not_to have_content 'merge_lfs'
+ expect(page).not_to have_content 'merge-test'
expect(page).to have_content 'fix'
expect(page).to have_content 'markdown'
expect(count_merge_requests).to eq(2)
@@ -57,14 +55,14 @@ describe 'Projects > Merge requests > User lists merge requests' do
visit_merge_requests(project, sort: sort_value_recently_created)
expect(first_merge_request).to include('fix')
- expect(last_merge_request).to include('merge_lfs')
+ 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_lfs')
+ expect(first_merge_request).to include('merge-test')
expect(last_merge_request).to include('fix')
expect(count_merge_requests).to eq(3)
end
@@ -72,7 +70,7 @@ describe 'Projects > Merge requests > User lists merge requests' do
it 'sorts by last updated' do
visit_merge_requests(project, sort: sort_value_recently_updated)
- expect(first_merge_request).to include('merge_lfs')
+ expect(first_merge_request).to include('merge-test')
expect(count_merge_requests).to eq(3)
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 1cfd78663e5..442ce14eb7e 100644
--- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
@@ -6,6 +6,8 @@ feature 'Merge requests > User posts diff notes', :js do
let(:project) { merge_request.source_project }
before do
+ allow_any_instance_of(ApplicationHelper).to receive(:collapsed_sidebar?).and_return(true)
+
project.add_developer(user)
sign_in(user)
end
@@ -71,7 +73,7 @@ feature 'Merge requests > User posts diff notes', :js do
end
context 'with an unfolded line' do
- before(:each) do
+ before do
find('.js-unfold', match: :first).click
wait_for_requests
end
@@ -95,6 +97,16 @@ feature 'Merge requests > User posts diff notes', :js do
visit diffs_project_merge_request_path(project, merge_request, view: 'inline')
end
+ context 'after deleteing a note' do
+ it 'allows commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+
+ first('.js-note-delete', visible: false).trigger('click')
+
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ end
+ end
+
context 'with a new line' do
it 'allows commenting' do
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
@@ -120,7 +132,7 @@ feature 'Merge requests > User posts diff notes', :js do
end
context 'with an unfolded line' do
- before(:each) do
+ before do
find('.js-unfold', match: :first).click
wait_for_requests
end
diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb
index 74d21822a59..d7cda73ab40 100644
--- a/spec/features/merge_requests/user_posts_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_notes_spec.rb
@@ -75,7 +75,6 @@ describe 'Merge requests > User posts notes', :js do
describe 'editing the note' do
before do
find('.note').hover
- open_more_actions_dropdown(note)
find('.js-note-edit').click
end
@@ -104,7 +103,6 @@ describe 'Merge requests > User posts notes', :js do
wait_for_requests
find('.note').hover
- open_more_actions_dropdown(note)
find('.js-note-edit').click
@@ -132,7 +130,6 @@ describe 'Merge requests > User posts notes', :js do
describe 'deleting an attachment' do
before do
find('.note').hover
- open_more_actions_dropdown(note)
find('.js-note-edit').click
end
diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb
index 20303359c46..624f13922ed 100644
--- a/spec/features/milestones/show_spec.rb
+++ b/spec/features/milestones/show_spec.rb
@@ -8,7 +8,7 @@ describe 'Milestone show' do
let(:issue_params) { { project: project, assignees: [user], author: user, milestone: milestone, labels: labels } }
before do
- project.add_user(user, :developer)
+ project.add_user(user, :developer)
sign_in(user)
end
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index a22d548eef3..96f6df587e1 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -11,10 +11,14 @@ feature 'Member autocomplete', :js do
sign_in(user)
end
- shared_examples "open suggestions when typing @" do
+ shared_examples "open suggestions when typing @" do |resource_name|
before do
page.within('.new-note') do
- find('#note_note').send_keys('@')
+ if resource_name == 'issue'
+ find('#note-body').send_keys('@')
+ else
+ find('#note_note').send_keys('@')
+ end
end
end
@@ -32,7 +36,7 @@ feature 'Member autocomplete', :js do
visit project_issue_path(project, noteable)
end
- include_examples "open suggestions when typing @"
+ include_examples "open suggestions when typing @", 'issue'
end
context 'adding a new note on a Merge Request' do
@@ -45,7 +49,7 @@ feature 'Member autocomplete', :js do
visit project_merge_request_path(project, noteable)
end
- include_examples "open suggestions when typing @"
+ include_examples "open suggestions when typing @", 'merge_request'
end
context 'adding a new note on a Commit' do
@@ -60,6 +64,6 @@ feature 'Member autocomplete', :js do
visit project_commit_path(project, noteable)
end
- include_examples "open suggestions when typing @"
+ include_examples "open suggestions when typing @", 'commit'
end
end
diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb
index 5e1e7dc078f..b45972b7f6b 100644
--- a/spec/features/password_reset_spec.rb
+++ b/spec/features/password_reset_spec.rb
@@ -16,7 +16,7 @@ feature 'Password reset' do
user.send_reset_password_instructions
user.update_attribute(:reset_password_sent_at, 5.minutes.ago)
- expect{ forgot_password(user) }.to change{ user.reset_password_sent_at }
+ expect { forgot_password(user) }.to change { user.reset_password_sent_at }
expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions'))
expect(current_path).to eq new_user_session_path
end
@@ -27,7 +27,7 @@ feature 'Password reset' do
# Reload because PG handles datetime less precisely than Ruby/Rails
user.reload
- expect{ forgot_password(user) }.not_to change{ user.reset_password_sent_at }
+ expect { forgot_password(user) }.not_to change { user.reset_password_sent_at }
expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions'))
expect(current_path).to eq new_user_session_path
end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index 672022304da..f183dd8cb75 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -7,9 +7,8 @@ describe 'Profile account page' do
sign_in(user)
end
- describe 'when signup is enabled' do
+ describe 'when I delete my account' do
before do
- stub_application_setting(signup_enabled: true)
visit profile_account_path
end
@@ -21,18 +20,6 @@ describe 'Profile account page' do
end
end
- describe 'when signup is disabled' do
- before do
- stub_application_setting(signup_enabled: false)
- visit profile_account_path
- end
-
- it 'does not have option to remove account' do
- expect(page).not_to have_content('Remove account')
- expect(current_path).to eq(profile_account_path)
- end
- end
-
describe 'when I reset private token' do
before do
visit profile_account_path
diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb
index 9944a6e1ff1..171e061e60e 100644
--- a/spec/features/profiles/account_spec.rb
+++ b/spec/features/profiles/account_spec.rb
@@ -35,7 +35,7 @@ feature 'Profile > Account' do
TestEnv.clean_test_path
end
- after(:example) do
+ after do
TestEnv.clean_test_path
end
@@ -43,14 +43,14 @@ feature 'Profile > Account' do
update_username(new_username)
visit new_project_path
expect(current_path).to eq(new_project_path)
- expect(find('h1.title')).to have_content(project.path)
+ expect(find('.breadcrumbs-sub-title')).to have_content(project.path)
end
scenario 'the old project path redirects to the new path' do
update_username(new_username)
visit old_project_path
expect(current_path).to eq(new_project_path)
- expect(find('h1.title')).to have_content(project.path)
+ expect(find('.breadcrumbs-sub-title')).to have_content(project.path)
end
end
end
diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb
index 6edc482b47e..623e4f341c5 100644
--- a/spec/features/profiles/gpg_keys_spec.rb
+++ b/spec/features/profiles/gpg_keys_spec.rb
@@ -42,7 +42,7 @@ feature 'Profile > GPG Keys' do
scenario 'User revokes a key via the key index' do
gpg_key = create :gpg_key, user: user, key: GpgHelpers::User2.public_key
- gpg_signature = create :gpg_signature, gpg_key: gpg_key, valid_signature: true
+ gpg_signature = create :gpg_signature, gpg_key: gpg_key, verification_status: :verified
visit profile_gpg_keys_path
@@ -51,7 +51,7 @@ feature 'Profile > GPG Keys' do
expect(page).to have_content('Your GPG keys (0)')
expect(gpg_signature.reload).to have_attributes(
- valid_signature: false,
+ verification_status: 'unknown_key',
gpg_key: nil
)
end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index 6541ea6bf57..aa71c4dbba4 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -28,6 +28,23 @@ feature 'Profile > SSH Keys' do
expect(page).to have_content("Title: #{attrs[:title]}")
expect(page).to have_content(attrs[:key])
end
+
+ context 'when only DSA and ECDSA keys are allowed' do
+ before do
+ forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE
+ stub_application_setting(rsa_key_restriction: forbidden, ed25519_key_restriction: forbidden)
+ end
+
+ scenario 'shows a validation error' do
+ attrs = attributes_for(:key)
+
+ fill_in('Key', with: attrs[:key])
+ fill_in('Title', with: attrs[:title])
+ click_button('Add key')
+
+ expect(page).to have_content('Key type is forbidden. Must be DSA or ECDSA')
+ end
+ end
end
scenario 'User sees their keys' do
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index 2c757f99a27..225d4c16841 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -53,12 +53,12 @@ describe 'Profile > Password' do
context 'Regular user' do
let(:user) { create(:user) }
- it 'renders 404 when sign-in is disabled' do
+ it 'renders 200 when sign-in is disabled' do
stub_application_setting(password_authentication_enabled: false)
visit edit_profile_password_path
- expect(page).to have_http_status(404)
+ expect(page).to have_http_status(200)
end
end
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index 9123aa9d155..c935cdfd5c4 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Profile > Preferences' do
+describe 'Profile > Preferences', :js do
let(:user) { create(:user) }
before do
@@ -8,28 +8,32 @@ describe 'Profile > Preferences' do
visit profile_preferences_path
end
- describe 'User changes their syntax highlighting theme', js: true do
+ 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'
- allowing_for_delay do
- visit page.current_path
- expect(page).to have_checked_field('user_color_scheme_id_5')
- end
+ wait_for_requests
+ refresh
+
+ expect(page).to have_checked_field('user_color_scheme_id_5')
end
end
- describe 'User changes their default dashboard', js: true do
+ 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
@@ -37,12 +41,12 @@ describe 'Profile > Preferences' do
select 'Starred Projects', from: 'user_dashboard'
click_button 'Save'
- allowing_for_delay do
- find('#logo').click
+ 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
- end
+ 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')
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index 2ef74e8857c..807a2189cc4 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -1,13 +1,8 @@
require 'rails_helper'
feature 'Mini Pipeline Graph in Commit View', :js do
- let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
- before do
- sign_in(user)
- end
-
context 'when commit has pipelines' do
let(:pipeline) do
create(:ci_empty_pipeline,
@@ -15,21 +10,14 @@ feature 'Mini Pipeline Graph in Commit View', :js do
ref: project.default_branch,
sha: project.commit.sha)
end
+ let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:build) do
- create(:ci_build, pipeline: pipeline)
- end
-
- before do
+ it 'displays a mini pipeline graph' do
build.run
visit project_commit_path(project, project.commit.id)
- end
- it 'should display a mini pipeline graph' do
expect(page).to have_selector('.mr-widget-pipeline-graph')
- end
- it 'should show the builds list when stage is clicked' do
first('.mini-pipeline-graph-dropdown-toggle').click
wait_for_requests
@@ -38,6 +26,8 @@ feature 'Mini Pipeline Graph in Commit View', :js do
expect(page).to have_selector('.ci-status-icon-running')
expect(page).to have_content(build.stage)
end
+
+ build.drop
end
end
diff --git a/spec/features/projects/commit/rss_spec.rb b/spec/features/projects/commits/rss_spec.rb
index db958346f06..db958346f06 100644
--- a/spec/features/projects/commit/rss_spec.rb
+++ b/spec/features/projects/commits/rss_spec.rb
diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb
new file mode 100644
index 00000000000..41f3c15a94c
--- /dev/null
+++ b/spec/features/projects/commits/user_browses_commits_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'User broweses commits' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'primary email' do
+ it 'finds a commit by a primary email' do
+ user = create(:user, email: 'dmitriy.zaporozhets@gmail.com')
+
+ visit(project_commit_path(project, RepoHelpers.sample_commit.id))
+
+ check_author_link(RepoHelpers.sample_commit.author_email, user)
+ end
+ end
+
+ context 'secondary email' do
+ it 'finds a commit by a secondary email' do
+ user =
+ create(:user) do |user|
+ create(:email, { user: user, email: 'dmitriy.zaporozhets@gmail.com' })
+ end
+
+ visit(project_commit_path(project, RepoHelpers.sample_commit.parent_id))
+
+ check_author_link(RepoHelpers.sample_commit.author_email, user)
+ end
+ end
+end
+
+private
+
+def check_author_link(email, author)
+ author_link = find('.commit-author-link')
+
+ expect(author_link['href']).to eq(user_path(author))
+ expect(author_link['title']).to eq(email)
+ expect(find('.commit-author-name').text).to eq(author.name)
+end
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
index d3b1d1f7be3..17f914c9c17 100644
--- a/spec/features/projects/edit_spec.rb
+++ b/spec/features/projects/edit_spec.rb
@@ -1,20 +1,21 @@
require 'rails_helper'
feature 'Project edit', js: true do
+ let(:admin) { create(:admin) }
let(:user) { create(:user) }
let(:project) { create(:project) }
- before do
- project.team << [user, :master]
- sign_in(user)
+ context 'feature visibility' do
+ before do
+ project.team << [user, :master]
+ sign_in(user)
- visit edit_project_path(project)
- end
+ visit edit_project_path(project)
+ end
- context 'feature visibility' do
context 'merge requests select' do
it 'hides merge requests section' do
- select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level')
+ find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
expect(page).to have_selector('.merge-requests-feature', visible: false)
end
@@ -30,7 +31,7 @@ feature 'Project edit', js: true do
context 'builds select' do
it 'hides builds select section' do
- select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
+ find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click
expect(page).to have_selector('.builds-feature', visible: false)
end
@@ -44,4 +45,18 @@ feature 'Project edit', js: true do
end
end
end
+
+ context 'LFS enabled setting' do
+ before do
+ sign_in(admin)
+ end
+
+ it 'displays the correct elements' do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ visit edit_project_path(project)
+
+ expect(page).to have_content('Git Large File Storage')
+ expect(page).to have_selector('input[name="project[lfs_enabled]"] + button', visible: true)
+ end
+ end
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 24691629063..57722276d79 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -19,21 +19,16 @@ describe 'Edit Project Settings' do
it 'toggles visibility' do
visit edit_project_path(project)
- select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level"
+ # disable by clicking toggle
+ toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
click_button 'Save changes'
end
wait_for_requests
expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
- select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level"
- page.within('.sharing-permissions') do
- click_button 'Save changes'
- end
- wait_for_requests
- expect(page).to have_selector(".shortcuts-#{shortcut_name}")
-
- select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level"
+ # re-enable by clicking toggle again
+ toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
click_button 'Save changes'
end
@@ -176,19 +171,19 @@ describe 'Edit Project Settings' do
end
it "disables repository related features" do
- select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+ toggle_feature_off('project[project_feature_attributes][repository_access_level]')
page.within('.sharing-permissions') do
click_button "Save changes"
end
- expect(find(".sharing-permissions")).to have_selector("select.disabled", count: 2)
+ expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.disabled", count: 2)
end
it "shows empty features project homepage" do
- select "Disabled", from: "project_project_feature_attributes_repository_access_level"
- select "Disabled", from: "project_project_feature_attributes_issues_access_level"
- select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
+ toggle_feature_off('project[project_feature_attributes][repository_access_level]')
+ toggle_feature_off('project[project_feature_attributes][issues_access_level]')
+ toggle_feature_off('project[project_feature_attributes][wiki_access_level]')
page.within('.sharing-permissions') do
click_button "Save changes"
@@ -201,9 +196,9 @@ describe 'Edit Project Settings' do
end
it "hides project activity tabs" do
- select "Disabled", from: "project_project_feature_attributes_repository_access_level"
- select "Disabled", from: "project_project_feature_attributes_issues_access_level"
- select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
+ toggle_feature_off('project[project_feature_attributes][repository_access_level]')
+ toggle_feature_off('project[project_feature_attributes][issues_access_level]')
+ toggle_feature_off('project[project_feature_attributes][wiki_access_level]')
page.within('.sharing-permissions') do
click_button "Save changes"
@@ -222,7 +217,7 @@ describe 'Edit Project Settings' do
# Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/25272
it "hides comments activity tab only on disabled issues, merge requests and repository" do
- select "Disabled", from: "project_project_feature_attributes_issues_access_level"
+ toggle_feature_off('project[project_feature_attributes][issues_access_level]')
save_changes_and_check_activity_tab do
expect(page).to have_content("Comments")
@@ -230,7 +225,7 @@ describe 'Edit Project Settings' do
visit edit_project_path(project)
- select "Disabled", from: "project_project_feature_attributes_merge_requests_access_level"
+ toggle_feature_off('project[project_feature_attributes][merge_requests_access_level]')
save_changes_and_check_activity_tab do
expect(page).to have_content("Comments")
@@ -238,7 +233,7 @@ describe 'Edit Project Settings' do
visit edit_project_path(project)
- select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+ toggle_feature_off('project[project_feature_attributes][repository_access_level]')
save_changes_and_check_activity_tab do
expect(page).not_to have_content("Comments")
@@ -275,4 +270,12 @@ describe 'Edit Project Settings' do
expect(page).not_to have_selector('.project-stats')
end
end
+
+ def toggle_feature_off(feature_name)
+ find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.checked").click
+ end
+
+ def toggle_feature_on(feature_name)
+ find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.checked)").click
+ end
end
diff --git a/spec/features/projects/files/creating_a_file_spec.rb b/spec/features/projects/files/creating_a_file_spec.rb
index e13bf4b6089..e1852a6e544 100644
--- a/spec/features/projects/files/creating_a_file_spec.rb
+++ b/spec/features/projects/files/creating_a_file_spec.rb
@@ -14,7 +14,7 @@ feature 'User wants to create a file' do
file_name = find('#file_name')
file_name.set options[:file_name] || 'README.md'
- file_content = find('#file-content')
+ file_content = find('#file-content', visible: false)
file_content.set options[:file_content] || 'Some content'
click_button 'Commit changes'
diff --git a/spec/features/projects/files/find_files_spec.rb b/spec/features/projects/files/find_files_spec.rb
deleted file mode 100644
index 57d67b28920..00000000000
--- a/spec/features/projects/files/find_files_spec.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-require 'spec_helper'
-
-feature 'Find files button in the tree header' do
- given(:user) { create(:user) }
- given(:project) { create(:project, :repository) }
-
- background do
- sign_in(user)
- project.team << [user, :developer]
- end
-
- scenario 'project main screen' do
- visit project_path(project)
-
- expect(page).to have_selector('.tree-controls .shortcuts-find-file')
- end
-
- scenario 'project tree screen' do
- visit project_tree_path(project, project.default_branch)
-
- expect(page).to have_selector('.tree-controls .shortcuts-find-file')
- end
-end
diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb
index 4238d25e9ee..9bcd5beabb8 100644
--- a/spec/features/projects/files/undo_template_spec.rb
+++ b/spec/features/projects/files/undo_template_spec.rb
@@ -20,7 +20,7 @@ feature 'Template Undo Button', js: true do
end
end
- context 'creating a non-matching file' do
+ context 'creating a non-matching file' do
before do
visit project_new_blob_path(project, 'master')
select_file_template_type('LICENSE')
diff --git a/spec/features/projects/files/user_searches_for_files_spec.rb b/spec/features/projects/files/user_searches_for_files_spec.rb
new file mode 100644
index 00000000000..a105685bca7
--- /dev/null
+++ b/spec/features/projects/files/user_searches_for_files_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe 'User searches for files' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'project main screen' do
+ context 'when project is empty' do
+ let(:empty_project) { create(:project) }
+
+ before do
+ empty_project.add_developer(user)
+ visit project_path(empty_project)
+ end
+
+ it 'does not show any result' do
+ fill_in('search', with: 'coffee')
+ click_button('Go')
+
+ expect(page).to have_content("We couldn't find any")
+ end
+ end
+
+ context 'when project is not empty' do
+ before do
+ project.add_developer(user)
+ visit project_path(project)
+ end
+
+ it 'shows "Find file" button' do
+ expect(page).to have_selector('.tree-controls .shortcuts-find-file')
+ end
+ end
+ end
+
+ describe 'project tree screen' do
+ before do
+ project.add_developer(user)
+ visit project_tree_path(project, project.default_branch)
+ end
+
+ it 'shows "Find file" button' do
+ expect(page).to have_selector('.tree-controls .shortcuts-find-file')
+ end
+
+ it 'shows found files' do
+ fill_in('search', with: 'coffee')
+ click_button('Go')
+
+ expect(page).to have_content('coffee')
+ expect(page).to have_content('CONTRIBUTING.md')
+ end
+ end
+end
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
deleted file mode 100644
index 5195d027a9f..00000000000
--- a/spec/features/projects/group_links_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'spec_helper'
-
-feature 'Project group links', :js do
- include Select2Helper
-
- let(:master) { create(:user) }
- let(:project) { create(:project) }
- let!(:group) { create(:group) }
-
- background do
- project.add_master(master)
- sign_in(master)
- end
-
- context 'setting an expiration date for a group link' do
- before do
- visit project_settings_members_path(project)
-
- click_on 'share-with-group-tab'
-
- select2 group.id, from: '#link_group_id'
- fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
- page.find('body').click
- find('.btn-create').trigger('click')
- end
-
- it 'shows the expiration time with a warning class' do
- page.within('.project-members-groups') do
- expect(page).to have_content('Expires in 4 days')
- expect(page).to have_selector('.text-warning')
- end
- end
- end
-
- context 'nested group project' do
- let!(:nested_group) { create(:group, parent: group) }
- let!(:another_group) { create(:group) }
- let!(:project) { create(:project, namespace: nested_group) }
-
- background do
- group.add_master(master)
- another_group.add_master(master)
- end
-
- it 'does not show ancestors', :nested_groups do
- visit project_settings_members_path(project)
-
- click_on 'share-with-group-tab'
- click_link 'Search for a group'
-
- page.within '.select2-drop' do
- expect(page).to have_content(another_group.name)
- expect(page).not_to have_content(group.name)
- end
- end
- end
-
- describe 'the groups dropdown' do
- before do
- group_two = create(:group)
- group.add_owner(master)
- group_two.add_owner(master)
-
- visit project_settings_members_path(project)
- execute_script 'GroupsSelect.PER_PAGE = 1;'
- open_select2 '#link_group_id'
- end
-
- it 'should infinitely scroll' do
- expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
-
- scroll_select2_to_bottom('.select2-drop .select2-results:visible')
-
- expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
- end
- end
-end
diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb
index 2385e1d9333..98c7ef57a51 100644
--- a/spec/features/projects/guest_navigation_menu_spec.rb
+++ b/spec/features/projects/guest_navigation_menu_spec.rb
@@ -13,8 +13,8 @@ describe 'Guest navigation menu' do
it 'shows allowed tabs only' do
visit project_path(project)
- within('.layout-nav') do
- expect(page).to have_content 'Project'
+ within('.nav-sidebar') do
+ expect(page).to have_content 'Overview'
expect(page).to have_content 'Issues'
expect(page).to have_content 'Wiki'
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 24e7843db63..e5c7781a096 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -3,72 +3,85 @@ require 'spec_helper'
feature 'Import/Export - project import integration test', js: true do
include Select2Helper
+ let(:user) { create(:user) }
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ gitlab_sign_in(user)
end
- after(:each) do
+ after do
FileUtils.rm_rf(export_path, secure: true)
end
context 'when selecting the namespace' do
let(:user) { create(:admin) }
- let!(:namespace) { create(:namespace, name: "asd", owner: user) }
+ let!(:namespace) { user.namespace }
+ let(:project_path) { 'test-project-path' + SecureRandom.hex }
- before do
- gitlab_sign_in(user)
- end
+ context 'prefilled the path' do
+ scenario 'user imports an exported project successfully' do
+ visit new_project_path
- scenario 'user imports an exported project successfully' do
- visit new_project_path
+ select2(namespace.id, from: '#project_namespace_id')
+ fill_in :project_path, with: project_path, visible: true
+ click_link 'GitLab export'
- select2(namespace.id, from: '#project_namespace_id')
- fill_in :project_path, with: 'test-project-path', visible: true
- click_link 'GitLab export'
+ expect(page).to have_content('Import an exported GitLab project')
+ expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=#{project_path}")
+ expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\h*\z/).and_call_original
- expect(page).to have_content('Import an exported GitLab project')
- expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=test-project-path")
- expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\z/).and_call_original
+ attach_file('file', file)
+ click_on 'Import project'
- attach_file('file', file)
+ expect(Project.count).to eq(1)
- expect { click_on 'Import project' }.to change { Project.count }.from(0).to(1)
-
- project = Project.last
- expect(project).not_to be_nil
- expect(project.issues).not_to be_empty
- expect(project.merge_requests).not_to be_empty
- expect(project_hook_exists?(project)).to be true
- expect(wiki_exists?(project)).to be true
- expect(project.import_status).to eq('finished')
+ project = Project.last
+ expect(project).not_to be_nil
+ expect(project.issues).not_to be_empty
+ expect(project.merge_requests).not_to be_empty
+ expect(project_hook_exists?(project)).to be true
+ expect(wiki_exists?(project)).to be true
+ expect(project.import_status).to eq('finished')
+ end
end
- scenario 'invalid project' do
- project = create(:project, namespace: namespace)
+ context 'path is not prefilled' do
+ scenario 'user imports an exported project successfully' do
+ visit new_project_path
+ click_link 'GitLab export'
- visit new_project_path
+ fill_in :path, with: 'test-project-path', visible: true
+ attach_file('file', file)
- select2(namespace.id, from: '#project_namespace_id')
- fill_in :project_path, with: project.name, visible: true
- click_link 'GitLab export'
- attach_file('file', file)
- click_on 'Import project'
+ expect { click_on 'Import project' }.to change { Project.count }.by(1)
- page.within('.flash-container') do
- expect(page).to have_content('Project could not be imported')
+ project = Project.last
+ expect(project).not_to be_nil
+ expect(page).to have_content("Project 'test-project-path' is being imported")
end
end
end
- context 'when limited to the default user namespace' do
- let(:user) { create(:user) }
- before do
- gitlab_sign_in(user)
+ scenario 'invalid project' do
+ project = create(:project, namespace: user.namespace)
+
+ visit new_project_path
+
+ select2(user.namespace.id, from: '#project_namespace_id')
+ fill_in :project_path, with: project.name, visible: true
+ click_link 'GitLab export'
+ attach_file('file', file)
+ click_on 'Import project'
+
+ page.within('.flash-container') do
+ expect(page).to have_content('Project could not be imported')
end
+ end
+ context 'when limited to the default user namespace' do
scenario 'passes correct namespace ID in the URL' do
visit new_project_path
@@ -87,6 +100,6 @@ feature 'Import/Export - project import integration test', js: true do
end
def project_hook_exists?(project)
- Gitlab::Git::Hook.new('post-receive', project).exists?
+ Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository).exists?
end
end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index e03e7b88174..9614c72cdc3 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/issuable_counts_caching_spec.rb b/spec/features/projects/issuable_counts_caching_spec.rb
deleted file mode 100644
index 1804d9dc244..00000000000
--- a/spec/features/projects/issuable_counts_caching_spec.rb
+++ /dev/null
@@ -1,132 +0,0 @@
-require 'spec_helper'
-
-describe 'Issuable counts caching', :use_clean_rails_memory_store_caching do
- let!(:member) { create(:user) }
- let!(:member_2) { create(:user) }
- let!(:non_member) { create(:user) }
- let!(:project) { create(:project, :public) }
- let!(:open_issue) { create(:issue, project: project) }
- let!(:confidential_issue) { create(:issue, :confidential, project: project, author: non_member) }
- let!(:closed_issue) { create(:issue, :closed, project: project) }
-
- before do
- project.add_developer(member)
- project.add_developer(member_2)
- end
-
- it 'caches issuable counts correctly for non-members' do
- # We can't use expect_any_instance_of because that uses a single instance.
- counts = 0
-
- allow_any_instance_of(IssuesFinder).to receive(:count_by_state).and_wrap_original do |m, *args|
- counts += 1
-
- m.call(*args)
- end
-
- aggregate_failures 'only counts once on first load with no params, and caches for later loads' do
- expect { visit project_issues_path(project) }
- .to change { counts }.by(1)
-
- expect { visit project_issues_path(project) }
- .not_to change { counts }
- end
-
- aggregate_failures 'uses counts from cache on load from non-member' do
- sign_in(non_member)
-
- expect { visit project_issues_path(project) }
- .not_to change { counts }
-
- sign_out(non_member)
- end
-
- aggregate_failures 'does not use the same cache for a member' do
- sign_in(member)
-
- expect { visit project_issues_path(project) }
- .to change { counts }.by(1)
-
- sign_out(member)
- end
-
- aggregate_failures 'uses the same cache for all members' do
- sign_in(member_2)
-
- expect { visit project_issues_path(project) }
- .not_to change { counts }
-
- sign_out(member_2)
- end
-
- aggregate_failures 'shares caches when params are passed' do
- expect { visit project_issues_path(project, author_username: non_member.username) }
- .to change { counts }.by(1)
-
- sign_in(member)
-
- expect { visit project_issues_path(project, author_username: non_member.username) }
- .to change { counts }.by(1)
-
- sign_in(non_member)
-
- expect { visit project_issues_path(project, author_username: non_member.username) }
- .not_to change { counts }
-
- sign_in(member_2)
-
- expect { visit project_issues_path(project, author_username: non_member.username) }
- .not_to change { counts }
-
- sign_out(member_2)
- end
-
- aggregate_failures 'resets caches on issue close' do
- Issues::CloseService.new(project, member).execute(open_issue)
-
- expect { visit project_issues_path(project) }
- .to change { counts }.by(1)
-
- sign_in(member)
-
- expect { visit project_issues_path(project) }
- .to change { counts }.by(1)
-
- sign_in(non_member)
-
- expect { visit project_issues_path(project) }
- .not_to change { counts }
-
- sign_in(member_2)
-
- expect { visit project_issues_path(project) }
- .not_to change { counts }
-
- sign_out(member_2)
- end
-
- aggregate_failures 'does not reset caches on issue update' do
- Issues::UpdateService.new(project, member, title: 'new title').execute(open_issue)
-
- expect { visit project_issues_path(project) }
- .not_to change { counts }
-
- sign_in(member)
-
- expect { visit project_issues_path(project) }
- .not_to change { counts }
-
- sign_in(non_member)
-
- expect { visit project_issues_path(project) }
- .not_to change { counts }
-
- sign_in(member_2)
-
- expect { visit project_issues_path(project) }
- .not_to change { counts }
-
- sign_out(member_2)
- end
- end
-end
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 037ac00d39f..3b5c6966287 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -292,26 +292,44 @@ feature 'Jobs' do
end
feature 'Variables' do
- let(:trigger_request) { create(:ci_trigger_request_with_variables) }
+ let(:trigger_request) { create(:ci_trigger_request) }
let(:job) do
create :ci_build, pipeline: pipeline, trigger_request: trigger_request
end
- before do
- visit project_job_path(project, job)
+ shared_examples 'expected variables behavior' do
+ it 'shows variable key and value after click', js: true 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')
+
+ click_button 'Reveal Variables'
+
+ expect(page).not_to have_css('.reveal-variables')
+ expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
+ expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
+ end
end
- it 'shows variable key and value after click', js: true 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')
+ context 'when variables are stored in trigger_request' do
+ before do
+ trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
- click_button 'Reveal Variables'
+ visit project_job_path(project, job)
+ end
+
+ it_behaves_like 'expected variables behavior'
+ end
+
+ context 'when variables are stored in pipeline_variables' do
+ before do
+ create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1')
+
+ visit project_job_path(project, job)
+ end
- expect(page).not_to have_css('.reveal-variables')
- expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
- expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
+ it_behaves_like 'expected variables behavior'
end
end
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb
deleted file mode 100644
index 1c348b987d4..00000000000
--- a/spec/features/projects/members/group_links_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'spec_helper'
-
-feature 'Projects > Members > Anonymous user sees members', js: true do
- let(:user) { create(:user) }
- let(:group) { create(:group, :public) }
- let(:project) { create(:project, :public) }
-
- background do
- project.team << [user, :master]
- @group_link = create(:project_group_link, project: project, group: group)
-
- sign_in(user)
- visit project_settings_members_path(project)
- end
-
- it 'updates group access level' do
- click_button @group_link.human_access
-
- page.within '.dropdown-menu' do
- click_link 'Guest'
- end
-
- wait_for_requests
-
- visit project_settings_members_path(project)
-
- expect(first('.group_member')).to have_content('Guest')
- end
-
- it 'updates expiry date' do
- tomorrow = Date.today + 3
-
- fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F")
- wait_for_requests
-
- page.within(find('li.group_member')) do
- expect(page).to have_content('Expires in')
- end
- end
-
- it 'deletes group link' do
- page.within(first('.group_member')) do
- find('.btn-remove').click
- end
- wait_for_requests
-
- expect(page).not_to have_selector('.group_member')
- end
-
- context 'search' do
- it 'finds no results' do
- page.within '.member-search-form' do
- fill_in 'search', with: 'testing 123'
- find('.member-search-btn').click
- end
-
- expect(page).not_to have_selector('.group_member')
- end
-
- it 'finds results' do
- page.within '.member-search-form' do
- fill_in 'search', with: group.name
- find('.member-search-btn').click
- end
-
- expect(page).to have_selector('.group_member', count: 1)
- end
- end
-end
diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
new file mode 100644
index 00000000000..9950272af08
--- /dev/null
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Groups with access list', js: true do
+ let(:user) { create(:user) }
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public) }
+
+ background do
+ project.team << [user, :master]
+ @group_link = create(:project_group_link, project: project, group: group)
+
+ sign_in(user)
+ visit project_settings_members_path(project)
+ end
+
+ scenario 'updates group access level' do
+ click_button @group_link.human_access
+
+ page.within '.dropdown-menu' do
+ click_link 'Guest'
+ end
+
+ wait_for_requests
+
+ visit project_settings_members_path(project)
+
+ expect(first('.group_member')).to have_content('Guest')
+ end
+
+ scenario 'updates expiry date' do
+ tomorrow = Date.today + 3
+
+ fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F")
+ wait_for_requests
+
+ page.within(find('li.group_member')) do
+ expect(page).to have_content('Expires in')
+ end
+ end
+
+ scenario 'deletes group link' do
+ page.within(first('.group_member')) do
+ find('.btn-remove').click
+ end
+ wait_for_requests
+
+ expect(page).not_to have_selector('.group_member')
+ end
+
+ context 'search in existing members (yes, this filters the groups list as well)' do
+ scenario 'finds no results' do
+ page.within '.member-search-form' do
+ fill_in 'search', with: 'testing 123'
+ find('.member-search-btn').click
+ end
+
+ expect(page).not_to have_selector('.group_member')
+ end
+
+ scenario 'finds results' do
+ page.within '.member-search-form' do
+ fill_in 'search', with: group.name
+ find('.member-search-btn').click
+ end
+
+ expect(page).to have_selector('.group_member', count: 1)
+ end
+ end
+end
diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb
new file mode 100644
index 00000000000..3b368f8e25d
--- /dev/null
+++ b/spec/features/projects/members/share_with_group_spec.rb
@@ -0,0 +1,191 @@
+require 'spec_helper'
+
+feature 'Project > Members > Share with Group', :js do
+ include Select2Helper
+ include ActionView::Helpers::DateHelper
+
+ let(:master) { create(:user) }
+
+ describe 'Share with group lock' do
+ shared_examples 'the project can be shared with groups' do
+ scenario 'the "Share with group" tab exists' do
+ visit project_settings_members_path(project)
+ expect(page).to have_selector('#share-with-group-tab')
+ end
+ end
+
+ shared_examples 'the project cannot be shared with groups' do
+ scenario 'the "Share with group" tab does not exist' do
+ visit project_settings_members_path(project)
+ expect(page).to have_selector('#add-member-tab')
+ expect(page).not_to have_selector('#share-with-group-tab')
+ end
+ end
+
+ context 'for a project in a root group' do
+ let!(:group_to_share_with) { create(:group) }
+ let(:project) { create(:project, namespace: create(:group)) }
+
+ background do
+ project.add_master(master)
+ sign_in(master)
+ end
+
+ context 'when the group has "Share with group lock" disabled' do
+ it_behaves_like 'the project can be shared with groups'
+
+ scenario 'the project can be shared with another group' do
+ visit project_settings_members_path(project)
+
+ click_on 'share-with-group-tab'
+
+ select2 group_to_share_with.id, from: '#link_group_id'
+ page.find('body').click
+ find('.btn-create').trigger('click')
+
+ page.within('.project-members-groups') do
+ expect(page).to have_content(group_to_share_with.name)
+ end
+ end
+ end
+
+ context 'when the group has "Share with group lock" enabled' do
+ before do
+ project.namespace.update_column(:share_with_group_lock, true)
+ end
+
+ it_behaves_like 'the project cannot be shared with groups'
+ end
+ end
+
+ context 'for a project in a subgroup', :nested_groups do
+ let!(:group_to_share_with) { create(:group) }
+ let(:root_group) { create(:group) }
+ let(:subgroup) { create(:group, parent: root_group) }
+ let(:project) { create(:project, namespace: subgroup) }
+
+ background do
+ project.add_master(master)
+ sign_in(master)
+ end
+
+ context 'when the root_group has "Share with group lock" disabled' do
+ context 'when the subgroup has "Share with group lock" disabled' do
+ it_behaves_like 'the project can be shared with groups'
+ end
+
+ context 'when the subgroup has "Share with group lock" enabled' do
+ before do
+ subgroup.update_column(:share_with_group_lock, true)
+ end
+
+ it_behaves_like 'the project cannot be shared with groups'
+ end
+ end
+
+ context 'when the root_group has "Share with group lock" enabled' do
+ before do
+ root_group.update_column(:share_with_group_lock, true)
+ end
+
+ context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do
+ it_behaves_like 'the project can be shared with groups'
+ end
+
+ context 'when the subgroup has "Share with group lock" enabled' do
+ before do
+ subgroup.update_column(:share_with_group_lock, true)
+ end
+
+ it_behaves_like 'the project cannot be shared with groups'
+ end
+ end
+ end
+ end
+
+ describe 'setting an expiration date for a group link' do
+ let(:project) { create(:project) }
+ let!(:group) { create(:group) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ project.add_master(master)
+ sign_in(master)
+
+ visit project_settings_members_path(project)
+
+ click_on 'share-with-group-tab'
+
+ select2 group.id, from: '#link_group_id'
+
+ fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
+ page.find('body').click
+ find('.btn-create').trigger('click')
+ end
+
+ scenario 'the group link shows the expiration time with a warning class' do
+ page.within('.project-members-groups') do
+ # Using distance_of_time_in_words_to_now because it is not the same as
+ # subtraction, and this way avoids time zone issues as well
+ expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at)
+ expect(page).to have_content(expires_in_text)
+ expect(page).to have_selector('.text-warning')
+ end
+ end
+ end
+
+ describe 'the groups dropdown' do
+ context 'with multiple groups to choose from' do
+ let(:project) { create(:project) }
+
+ background do
+ project.add_master(master)
+ sign_in(master)
+
+ create(:group).add_owner(master)
+ create(:group).add_owner(master)
+
+ visit project_settings_members_path(project)
+ execute_script 'GroupsSelect.PER_PAGE = 1;'
+ open_select2 '#link_group_id'
+ end
+
+ it 'should infinitely scroll' do
+ expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
+
+ scroll_select2_to_bottom('.select2-drop .select2-results:visible')
+
+ expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
+ end
+ end
+
+ context 'for a project in a nested group' do
+ let(:group) { create(:group) }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:group_to_share_with) { create(:group) }
+ let!(:project) { create(:project, namespace: nested_group) }
+
+ background do
+ project.add_master(master)
+ sign_in(master)
+ group.add_master(master)
+ group_to_share_with.add_master(master)
+ end
+
+ scenario 'the groups dropdown does not show ancestors', :nested_groups do
+ visit project_settings_members_path(project)
+
+ click_on 'share-with-group-tab'
+ click_link 'Search for a group'
+
+ page.within '.select2-drop' do
+ expect(page).to have_content(group_to_share_with.name)
+ expect(page).not_to have_content(group.name)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 24c9f708456..0fbe1ddb2a5 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Members > User requests access' do
+feature 'Projects > Members > User requests access', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :public, :access_requestable, :repository) }
let(:master) { project.owner }
@@ -46,11 +46,10 @@ feature 'Projects > Members > User requests access' do
expect(project.requesters.exists?(user_id: user)).to be_truthy
- page.within('.layout-nav .nav-links') do
+ page.within('.nav-sidebar') do
click_link('Members')
end
- visit project_project_members_path(project)
page.within('.content') do
expect(page).not_to have_content(user.name)
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 22fb1223739..cd3dc72d3c6 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'New project' do
+ include Select2Helper
+
let(:user) { create(:admin) }
before do
@@ -68,26 +70,10 @@ feature 'New project' do
expect(namespace.text).to eq group.name
end
-
- context 'on validation error' do
- before do
- fill_in('project_path', with: 'private-group-project')
- choose('Internal')
- click_button('Create project')
-
- expect(page).to have_css '.project-edit-errors .alert.alert-danger'
- end
-
- it 'selects the group namespace' do
- namespace = find('#project_namespace_id option[selected]')
-
- expect(namespace.text).to eq group.name
- end
- end
end
context 'with subgroup namespace' do
- let(:group) { create(:group, :private, owner: user) }
+ let(:group) { create(:group, owner: user) }
let(:subgroup) { create(:group, parent: group) }
before do
@@ -101,6 +87,41 @@ feature 'New project' do
expect(namespace.text).to eq subgroup.full_path
end
end
+
+ context 'when changing namespaces dynamically', :js do
+ let(:public_group) { create(:group, :public) }
+ let(:internal_group) { create(:group, :internal) }
+ let(:private_group) { create(:group, :private) }
+
+ before do
+ public_group.add_owner(user)
+ internal_group.add_owner(user)
+ private_group.add_owner(user)
+ visit new_project_path(namespace_id: public_group.id)
+ end
+
+ it 'enables the correct visibility options' do
+ select2(user.namespace_id, from: '#project_namespace_id')
+ expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).not_to be_disabled
+ expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::INTERNAL}")).not_to be_disabled
+ expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PUBLIC}")).not_to be_disabled
+
+ select2(public_group.id, from: '#project_namespace_id')
+ expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).not_to be_disabled
+ expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::INTERNAL}")).not_to be_disabled
+ expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PUBLIC}")).not_to be_disabled
+
+ select2(internal_group.id, from: '#project_namespace_id')
+ expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).not_to be_disabled
+ expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::INTERNAL}")).not_to be_disabled
+ expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PUBLIC}")).to be_disabled
+
+ select2(private_group.id, from: '#project_namespace_id')
+ expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).not_to be_disabled
+ expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::INTERNAL}")).to be_disabled
+ expect(find("#project_visibility_level_#{Gitlab::VisibilityLevel::PUBLIC}")).to be_disabled
+ end
+ end
end
context 'Import project options' do
diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb
index 7d4ec2b4e68..5d77cd1ccd5 100644
--- a/spec/features/projects/project_settings_spec.rb
+++ b/spec/features/projects/project_settings_spec.rb
@@ -46,7 +46,7 @@ describe 'Edit Project Settings' do
context 'when changing project name' do
it 'renames the repository' do
rename_project(project, name: 'bar')
- expect(find('h1.title')).to have_content(project.name)
+ expect(find('.breadcrumbs')).to have_content(project.name)
end
context 'with emojis' do
@@ -65,7 +65,7 @@ describe 'Edit Project Settings' do
TestEnv.clean_test_path
end
- after(:example) do
+ after do
TestEnv.clean_test_path
end
@@ -74,7 +74,7 @@ describe 'Edit Project Settings' do
new_path = namespace_project_path(project.namespace, 'bar')
visit new_path
expect(current_path).to eq(new_path)
- expect(find('h1.title')).to have_content(project.name)
+ expect(find('.breadcrumbs')).to have_content(project.name)
end
specify 'the project is accessible via a redirect from the old path' do
@@ -83,7 +83,7 @@ describe 'Edit Project Settings' do
new_path = namespace_project_path(project.namespace, 'bar')
visit old_path
expect(current_path).to eq(new_path)
- expect(find('h1.title')).to have_content(project.name)
+ expect(find('.breadcrumbs')).to have_content(project.name)
end
context 'and a new project is added with the same path' do
@@ -93,7 +93,7 @@ describe 'Edit Project Settings' do
new_project = create(:project, namespace: user.namespace, path: 'gitlabhq', name: 'quz')
visit old_path
expect(current_path).to eq(old_path)
- expect(find('h1.title')).to have_content(new_project.name)
+ expect(find('.breadcrumbs')).to have_content(new_project.name)
end
end
end
@@ -107,11 +107,11 @@ describe 'Edit Project Settings' do
TestEnv.clean_test_path
end
- before(:example) do
+ before do
group.add_owner(user)
end
- after(:example) do
+ after do
TestEnv.clean_test_path
end
@@ -120,7 +120,7 @@ describe 'Edit Project Settings' do
new_path = namespace_project_path(group, project)
visit new_path
expect(current_path).to eq(new_path)
- expect(find('h1.title')).to have_content(project.name)
+ expect(find('.breadcrumbs')).to have_content(project.name)
end
specify 'the project is accessible via a redirect from the old path' do
@@ -129,7 +129,7 @@ describe 'Edit Project Settings' do
new_path = namespace_project_path(group, project)
visit old_path
expect(current_path).to eq(new_path)
- expect(find('h1.title')).to have_content(project.name)
+ expect(find('.breadcrumbs')).to have_content(project.name)
end
context 'and a new project is added with the same path' do
@@ -139,7 +139,7 @@ describe 'Edit Project Settings' do
new_project = create(:project, namespace: user.namespace, path: 'gitlabhq', name: 'quz')
visit old_path
expect(current_path).to eq(old_path)
- expect(find('h1.title')).to have_content(new_project.name)
+ expect(find('.breadcrumbs')).to have_content(new_project.name)
end
end
end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index 104ce08d9f3..b1ec556bf16 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -19,8 +19,8 @@ feature 'Project settings > Merge Requests', :js do
expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
- select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level"
within('.sharing-permissions-form') do
+ find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
click_on('Save changes')
end
@@ -39,8 +39,8 @@ feature 'Project settings > Merge Requests', :js do
expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
- select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level"
within('.sharing-permissions-form') do
+ find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click
click_on('Save changes')
end
@@ -60,8 +60,8 @@ feature 'Project settings > Merge Requests', :js do
expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
- select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level"
within('.sharing-permissions-form') do
+ find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
click_on('Save changes')
end
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index 232d796a200..975d204e75e 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -41,5 +41,15 @@ feature "Pipelines settings" do
checkbox = find_field('project_auto_cancel_pending_pipelines')
expect(checkbox).to be_checked
end
+
+ scenario 'update auto devops settings' do
+ fill_in('project_auto_devops_attributes_domain', with: 'test.com')
+ page.choose('project_auto_devops_attributes_enabled_false')
+ click_on 'Save changes'
+
+ expect(page.status_code).to eq(200)
+ expect(project.auto_devops).to be_present
+ expect(project.auto_devops).not_to be_enabled
+ end
end
end
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
index 1756c7d00fe..37ee6255bd1 100644
--- a/spec/features/projects/settings/visibility_settings_spec.rb
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -11,19 +11,19 @@ feature 'Visibility settings', js: true do
end
scenario 'project visibility select is available' do
- visibility_select_container = find('.js-visibility-select')
+ visibility_select_container = find('.project-visibility-setting')
- expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s
- expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
+ expect(visibility_select_container.find('select').value).to eq project.visibility_level.to_s
+ expect(visibility_select_container).to have_content 'The project can be accessed by anyone, regardless of authentication.'
end
scenario 'project visibility description updates on change' do
- visibility_select_container = find('.js-visibility-select')
- visibility_select = visibility_select_container.find('.visibility-select')
+ visibility_select_container = find('.project-visibility-setting')
+ visibility_select = visibility_select_container.find('select')
visibility_select.select('Private')
expect(visibility_select.value).to eq '0'
- expect(visibility_select_container).to have_content 'Project access must be granted explicitly to each user.'
+ expect(visibility_select_container).to have_content 'Access must be granted explicitly to each user.'
end
end
@@ -37,11 +37,10 @@ feature 'Visibility settings', js: true do
end
scenario 'project visibility is locked' do
- visibility_select_container = find('.js-visibility-select')
+ visibility_select_container = find('.project-visibility-setting')
- expect(visibility_select_container).not_to have_select '.visibility-select'
- expect(visibility_select_container).to have_content 'Public'
- expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
+ expect(visibility_select_container).to have_selector 'select[name="project[visibility_level]"]:disabled'
+ expect(visibility_select_container).to have_content 'The project can be accessed by anyone, regardless of authentication.'
end
end
end
diff --git a/spec/features/projects/sub_group_issuables_spec.rb b/spec/features/projects/sub_group_issuables_spec.rb
index aaf64d42515..eb2d3ff50a0 100644
--- a/spec/features/projects/sub_group_issuables_spec.rb
+++ b/spec/features/projects/sub_group_issuables_spec.rb
@@ -24,9 +24,8 @@ describe 'Subgroup Issuables', :js, :nested_groups do
end
def expect_to_have_full_subgroup_title
- title = find('.title-container')
+ title = find('.breadcrumbs-links')
- expect(title).not_to have_selector '.initializing'
- expect(title).to have_content 'group / subgroup / project'
+ expect(title).to have_content 'group subgroup project'
end
end
diff --git a/spec/features/projects/user_creates_files_spec.rb b/spec/features/projects/user_creates_files_spec.rb
index 4b78cc4fc53..3d335687510 100644
--- a/spec/features/projects/user_creates_files_spec.rb
+++ b/spec/features/projects/user_creates_files_spec.rb
@@ -56,11 +56,10 @@ describe 'User creates files' do
find('.add-to-tree').click
click_link('New file')
+ expect(page).to have_selector('.file-editor')
end
it 'creates and commit a new file', js: true do
- expect(page).to have_selector('.file-editor')
-
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)
diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb
index 8ae89c980b9..3129aad8473 100644
--- a/spec/features/projects/user_edits_files_spec.rb
+++ b/spec/features/projects/user_edits_files_spec.rb
@@ -1,10 +1,6 @@
require 'spec_helper'
describe 'User edits files' 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."
- end
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) }
@@ -24,6 +20,9 @@ describe 'User edits files' do
it 'inserts a content of a file', js: true do
click_link('.gitignore')
find('.js-edit-blob').click
+
+ wait_for_requests
+
execute_script("ace.edit('editor').setValue('*.rbca')")
expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca')
@@ -39,6 +38,9 @@ describe 'User edits files' do
it 'commits an edited file', js: true do
click_link('.gitignore')
find('.js-edit-blob').click
+
+ wait_for_requests
+
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
@@ -53,6 +55,9 @@ describe 'User edits files' do
it 'commits an edited file to a new branch', js: true do
click_link('.gitignore')
find('.js-edit-blob').click
+
+ wait_for_requests
+
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
fill_in(:branch_name, with: 'new_branch_name', visible: true)
@@ -69,6 +74,9 @@ describe 'User edits files' do
it 'shows the diff of an edited file', js: true do
click_link('.gitignore')
find('.js-edit-blob').click
+
+ wait_for_requests
+
execute_script("ace.edit('editor').setValue('*.rbca')")
click_link('Preview changes')
@@ -91,7 +99,12 @@ describe 'User edits files' do
click_link('Fork')
- expect(page).to have_content(fork_message)
+ expect(page).to have_content(
+ "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."
+ )
+
+ wait_for_requests
execute_script("ace.edit('editor').setValue('*.rbca')")
@@ -106,6 +119,9 @@ describe 'User edits files' do
expect(page).to have_button('Cancel')
click_link('Fork')
+
+ wait_for_requests
+
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
click_button('Commit changes')
diff --git a/spec/features/projects/user_interacts_with_stars_spec.rb b/spec/features/projects/user_interacts_with_stars_spec.rb
new file mode 100644
index 00000000000..0ac3f8181fa
--- /dev/null
+++ b/spec/features/projects/user_interacts_with_stars_spec.rb
@@ -0,0 +1,38 @@
+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
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ visit(project_path(project))
+ end
+
+ it 'toggles the star' do
+ find('.star-btn').click
+
+ expect(page).to have_css('.star-count', text: 1)
+
+ find('.star-btn').click
+
+ expect(page).to have_css('.star-count', text: 0)
+ end
+ end
+
+ context 'when user is not signed in' do
+ before do
+ visit(project_path(project))
+ end
+
+ it 'does not allow to star a project' do
+ expect(page).not_to have_content('.toggle-star')
+
+ find('.star-btn').click
+
+ expect(current_path).to eq(new_user_session_path)
+ end
+ end
+end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index d3d7915bebf..81f7ab80a04 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -18,7 +18,7 @@ feature 'Project' do
click_button "Create project"
end
- expect(page).to have_content 'This project Loading..'
+ expect(page).to have_content template.name
end
end
@@ -95,49 +95,6 @@ feature 'Project' do
end
end
- describe 'project title' do
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
-
- before do
- sign_in(user)
- project.add_user(user, Gitlab::Access::MASTER)
- visit project_path(project)
- end
-
- it 'clicks toggle and shows dropdown', js: true do
- find('.js-projects-dropdown-toggle').click
- expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 1)
- end
- end
-
- describe 'project title' do
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
- let(:project2) { create(:project, namespace: user.namespace, path: 'test') }
- let(:issue) { create(:issue, project: project) }
-
- context 'on issues page', js: true do
- before do
- sign_in(user)
- project.add_user(user, Gitlab::Access::MASTER)
- project2.add_user(user, Gitlab::Access::MASTER)
- visit project_issue_path(project, issue)
- end
-
- it 'clicks toggle and shows dropdown' do
- find('.js-projects-dropdown-toggle').click
- expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 2)
-
- page.within '.dropdown-menu-projects' do
- click_link project.name_with_namespace
- end
-
- expect(page).to have_content project.name
- end
- end
- end
-
describe 'tree view (default view is set to Files)' do
let(:user) { create(:user, project_view: 'files') }
let(:project) { create(:forked_project_with_submodules) }
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index c9ba1a8c088..8abd4403065 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projected Tags', js: true do
+feature 'Protected Tags', js: true do
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :repository) }
diff --git a/spec/features/reportable_note/commit_spec.rb b/spec/features/reportable_note/commit_spec.rb
index 3bf25221e36..9b6864eb90f 100644
--- a/spec/features/reportable_note/commit_spec.rb
+++ b/spec/features/reportable_note/commit_spec.rb
@@ -18,7 +18,7 @@ describe 'Reportable note on commit', :js do
visit project_commit_path(project, sample_commit.id)
end
- it_behaves_like 'reportable note'
+ it_behaves_like 'reportable note', 'commit'
end
context 'a diff note' do
@@ -28,6 +28,6 @@ describe 'Reportable note on commit', :js do
visit project_commit_path(project, sample_commit.id)
end
- it_behaves_like 'reportable note'
+ it_behaves_like 'reportable note', 'commit'
end
end
diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb
index 21e96f6f103..f5a1950e48e 100644
--- a/spec/features/reportable_note/issue_spec.rb
+++ b/spec/features/reportable_note/issue_spec.rb
@@ -13,5 +13,5 @@ describe 'Reportable note on issue', :js do
visit project_issue_path(project, issue)
end
- it_behaves_like 'reportable note'
+ it_behaves_like 'reportable note', 'issue'
end
diff --git a/spec/features/reportable_note/merge_request_spec.rb b/spec/features/reportable_note/merge_request_spec.rb
index bb296546e06..1f69257f7ed 100644
--- a/spec/features/reportable_note/merge_request_spec.rb
+++ b/spec/features/reportable_note/merge_request_spec.rb
@@ -15,12 +15,12 @@ describe 'Reportable note on merge request', :js do
context 'a normal note' do
let!(:note) { create(:note_on_merge_request, noteable: merge_request, project: project) }
- it_behaves_like 'reportable note'
+ it_behaves_like 'reportable note', 'merge_request'
end
context 'a diff note' do
let!(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
- it_behaves_like 'reportable note'
+ it_behaves_like 'reportable note', 'merge_request'
end
end
diff --git a/spec/features/reportable_note/snippets_spec.rb b/spec/features/reportable_note/snippets_spec.rb
index f1e48ed46be..98ef50b78de 100644
--- a/spec/features/reportable_note/snippets_spec.rb
+++ b/spec/features/reportable_note/snippets_spec.rb
@@ -17,6 +17,6 @@ describe 'Reportable note on snippets', :js do
visit project_snippet_path(project, snippet)
end
- it_behaves_like 'reportable note'
+ it_behaves_like 'reportable note', 'snippet'
end
end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index cac31c34ad1..c7f0e342809 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -1,149 +1,150 @@
require 'spec_helper'
-describe "Runners" do
- let(:user) { create(:user) }
+feature 'Runners' do
+ given(:user) { create(:user) }
- before do
+ background do
sign_in(user)
end
- describe "specific runners" do
- before do
- @project = FactoryGirl.create :project, shared_runners_enabled: false
- @project.team << [user, :master]
+ context 'when a project has enabled shared_runners' do
+ given(:project) { create(:project) }
- @project2 = FactoryGirl.create :project
- @project2.team << [user, :master]
+ background do
+ project.add_master(user)
+ end
- @project3 = FactoryGirl.create :project
- @project3.team << [user, :developer]
+ context 'when a specific runner is activated on the project' do
+ given(:specific_runner) { create(:ci_runner, :specific) }
- @shared_runner = FactoryGirl.create :ci_runner, :shared
- @specific_runner = FactoryGirl.create :ci_runner
- @specific_runner2 = FactoryGirl.create :ci_runner
- @specific_runner3 = FactoryGirl.create :ci_runner
- @project.runners << @specific_runner
- @project2.runners << @specific_runner2
- @project3.runners << @specific_runner3
+ background do
+ project.runners << specific_runner
+ end
- visit runners_path(@project)
- end
+ scenario 'user sees the specific runner' do
+ visit runners_path(project)
- before do
- expect(page).not_to have_content(@specific_runner3.display_name)
- expect(page).not_to have_content(@specific_runner3.display_name)
- end
+ within '.activated-specific-runners' do
+ expect(page).to have_content(specific_runner.display_name)
+ end
- it "places runners in right places" do
- expect(page.find(".available-specific-runners")).to have_content(@specific_runner2.display_name)
- expect(page.find(".activated-specific-runners")).to have_content(@specific_runner.display_name)
- expect(page.find(".available-shared-runners")).to have_content(@shared_runner.display_name)
- end
+ click_on specific_runner.short_sha
- it "enables specific runner for project" do
- within ".available-specific-runners" do
- click_on "Enable for this project"
+ expect(page).to have_content(specific_runner.platform)
end
- expect(page.find(".activated-specific-runners")).to have_content(@specific_runner2.display_name)
- end
+ scenario 'user removes an activated specific runner if this is last project for that runners' do
+ visit runners_path(project)
- it "disables specific runner for project" do
- @project2.runners << @specific_runner
- visit runners_path(@project)
+ within '.activated-specific-runners' do
+ click_on 'Remove Runner'
+ end
- within ".activated-specific-runners" do
- click_on "Disable for this project"
+ expect(page).not_to have_content(specific_runner.display_name)
end
- expect(page.find(".available-specific-runners")).to have_content(@specific_runner.display_name)
- end
+ scenario 'user edits the runner to be protected' do
+ visit runners_path(project)
- it "removes specific runner for project if this is last project for that runners" do
- within ".activated-specific-runners" do
- click_on "Remove Runner"
- end
+ within '.activated-specific-runners' do
+ first('.edit-runner > a').click
+ end
- expect(Ci::Runner.exists?(id: @specific_runner)).to be_falsey
- end
- end
+ expect(page.find_field('runner[access_level]')).not_to be_checked
- describe "shared runners" do
- before do
- @project = FactoryGirl.create :project, shared_runners_enabled: false
- @project.team << [user, :master]
- visit runners_path(@project)
- end
+ check 'runner_access_level'
+ click_button 'Save changes'
- it "enables shared runners" do
- click_on "Enable shared Runners"
- expect(@project.reload.shared_runners_enabled).to be_truthy
- end
- end
+ expect(page).to have_content 'Protected Yes'
+ end
- describe "shared runners description" do
- let(:shared_runners_text) { 'custom **shared** runners description' }
- let(:shared_runners_html) { 'custom shared runners description' }
+ context 'when a runner has a tag' do
+ background do
+ specific_runner.update(tag_list: ['tag'])
+ end
- before do
- stub_application_setting(shared_runners_text: shared_runners_text)
- project = FactoryGirl.create :project, shared_runners_enabled: false
- project.team << [user, :master]
- visit runners_path(project)
- end
+ scenario 'user edits runner not to run untagged jobs' do
+ visit runners_path(project)
- it "sees shared runners description" do
- expect(page.find(".shared-runners-description")).to have_content(shared_runners_html)
- end
- end
+ within '.activated-specific-runners' do
+ first('.edit-runner > a').click
+ end
- describe "show page" do
- before do
- @project = FactoryGirl.create :project
- @project.team << [user, :master]
- @specific_runner = FactoryGirl.create :ci_runner
- @project.runners << @specific_runner
- end
+ expect(page.find_field('runner[run_untagged]')).to be_checked
- it "shows runner information" do
- visit runners_path(@project)
- click_on @specific_runner.short_sha
- expect(page).to have_content(@specific_runner.platform)
- end
- end
+ uncheck 'runner_run_untagged'
+ click_button 'Save changes'
- feature 'configuring runners ability to picking untagged jobs' do
- given(:project) { create(:project) }
- given(:runner) { create(:ci_runner) }
+ expect(page).to have_content 'Can run untagged jobs No'
+ end
+ end
- background do
- project.team << [user, :master]
- project.runners << runner
- end
+ context 'when a shared runner is activated on the project' do
+ given!(:shared_runner) { create(:ci_runner, :shared) }
- scenario 'user checks default configuration' do
- visit project_runner_path(project, runner)
+ scenario 'user sees CI/CD setting page' do
+ visit runners_path(project)
- expect(page).to have_content 'Can run untagged jobs Yes'
+ expect(page.find('.available-shared-runners')).to have_content(shared_runner.display_name)
+ end
+ end
end
- context 'when runner has tags' do
- before do
- runner.update_attribute(:tag_list, ['tag'])
+ context 'when a specific runner exists in another project' do
+ given(:another_project) { create(:project) }
+ given(:specific_runner) { create(:ci_runner, :specific) }
+
+ background do
+ another_project.add_master(user)
+ another_project.runners << specific_runner
end
- scenario 'user wants to prevent runner from running untagged job' do
+ scenario 'user enables and disables a specific runner' do
visit runners_path(project)
- page.within('.activated-specific-runners') do
- first('small > a').click
+
+ within '.available-specific-runners' do
+ click_on 'Enable for this project'
end
- uncheck 'runner_run_untagged'
- click_button 'Save changes'
+ expect(page.find('.activated-specific-runners')).to have_content(specific_runner.display_name)
+
+ within '.activated-specific-runners' do
+ click_on 'Disable for this project'
+ end
+
+ expect(page.find('.available-specific-runners')).to have_content(specific_runner.display_name)
+ end
+ end
- expect(page).to have_content 'Can run untagged jobs No'
- expect(runner.reload.run_untagged?).to eq false
+ context 'when application settings have shared_runners_text' do
+ given(:shared_runners_text) { 'custom **shared** runners description' }
+ given(:shared_runners_html) { 'custom shared runners description' }
+
+ background do
+ stub_application_setting(shared_runners_text: shared_runners_text)
+ end
+
+ scenario 'user sees shared runners description' do
+ visit runners_path(project)
+
+ expect(page.find('.shared-runners-description')).to have_content(shared_runners_html)
end
end
end
+
+ context 'when a project has disabled shared_runners' do
+ given(:project) { create(:project, shared_runners_enabled: false) }
+
+ background do
+ project.add_master(user)
+ end
+
+ scenario 'user enables shared runners' do
+ visit runners_path(project)
+
+ click_on 'Enable shared Runners'
+
+ expect(page.find('.shared-runners-description')).to have_content('Disable shared Runners')
+ end
+ end
end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 9b49fc2225d..8f6d0bb9d1b 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -160,7 +160,7 @@ describe "Search" do
fill_in 'search', with: 'gitlab'
find('#search').native.send_keys(:enter)
- page.within '.title' do
+ page.within '.breadcrumbs-sub-title' do
expect(page).to have_content 'Search'
end
end
@@ -195,37 +195,33 @@ describe "Search" do
it 'takes user to her issues page when issues assigned is clicked' do
find('.dropdown-menu').click_link 'Issues assigned to me'
- sleep 2
expect(page).to have_selector('.filtered-search')
- expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ 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"
- sleep 2
expect(page).to have_selector('.filtered-search')
- expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ 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'
- sleep 2
expect(page).to have_selector('.merge-requests-holder')
- expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ 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"
- sleep 2
expect(page).to have_selector('.merge-requests-holder')
- expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_tokens([author_token(user.name)])
expect_filtered_search_input_empty
end
end
@@ -285,4 +281,30 @@ describe "Search" do
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/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
new file mode 100644
index 00000000000..8efa5b58141
--- /dev/null
+++ b/spec/features/signed_commits_spec.rb
@@ -0,0 +1,179 @@
+require 'spec_helper'
+
+describe 'GPG signed commits', :js do
+ let(:project) { create(:project, :repository) }
+
+ it 'changes from unverified to verified when the user changes his email to match the gpg key' do
+ user = create :user, email: 'unrelated.user@example.org'
+ project.team << [user, :master]
+
+ Sidekiq::Testing.inline! do
+ create :gpg_key, key: GpgHelpers::User1.public_key, user: user
+ end
+
+ sign_in(user)
+
+ visit project_commits_path(project, :'signed-commits')
+
+ within '#commits-list' do
+ expect(page).to have_content 'Unverified'
+ expect(page).not_to have_content 'Verified'
+ end
+
+ # user changes his email which makes the gpg key verified
+ Sidekiq::Testing.inline! do
+ user.skip_reconfirmation!
+ user.update_attributes!(email: GpgHelpers::User1.emails.first)
+ end
+
+ visit project_commits_path(project, :'signed-commits')
+
+ within '#commits-list' do
+ expect(page).to have_content 'Unverified'
+ expect(page).to have_content 'Verified'
+ end
+ end
+
+ it 'changes from unverified to verified when the user adds the missing gpg key' do
+ user = create :user, email: GpgHelpers::User1.emails.first
+ project.team << [user, :master]
+
+ sign_in(user)
+
+ visit project_commits_path(project, :'signed-commits')
+
+ within '#commits-list' do
+ expect(page).to have_content 'Unverified'
+ expect(page).not_to have_content 'Verified'
+ end
+
+ # user adds the gpg key which makes the signature valid
+ Sidekiq::Testing.inline! do
+ create :gpg_key, key: GpgHelpers::User1.public_key, user: user
+ end
+
+ visit project_commits_path(project, :'signed-commits')
+
+ within '#commits-list' do
+ expect(page).to have_content 'Unverified'
+ expect(page).to have_content 'Verified'
+ end
+ end
+
+ context 'shows popover badges' do
+ let(:user_1) do
+ create :user, email: GpgHelpers::User1.emails.first, username: 'nannie.bernhard', name: 'Nannie Bernhard'
+ end
+
+ let(:user_1_key) do
+ Sidekiq::Testing.inline! do
+ create :gpg_key, key: GpgHelpers::User1.public_key, user: user_1
+ end
+ end
+
+ let(:user_2) do
+ create(:user, email: GpgHelpers::User2.emails.first, username: 'bette.cartwright', name: 'Bette Cartwright').tap do |user|
+ # secondary, unverified email
+ create :email, user: user, email: GpgHelpers::User2.emails.last
+ end
+ end
+
+ let(:user_2_key) do
+ Sidekiq::Testing.inline! do
+ create :gpg_key, key: GpgHelpers::User2.public_key, user: user_2
+ end
+ end
+
+ before do
+ user = create :user
+ project.team << [user, :master]
+
+ sign_in(user)
+ end
+
+ it 'unverified signature' do
+ visit project_commits_path(project, :'signed-commits')
+
+ within(find('.commit', text: 'signed commit by bette cartwright')) do
+ click_on 'Unverified'
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with an unverified signature.'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
+ end
+ end
+ end
+
+ it 'unverified signature: user email does not match the committer email, but is the same user' do
+ user_2_key
+
+ visit project_commits_path(project, :'signed-commits')
+
+ within(find('.commit', text: 'signed and authored commit by bette cartwright, different email')) do
+ click_on 'Unverified'
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with a verified signature, but the committer email is not verified to belong to the same user.'
+ expect(page).to have_content 'Bette Cartwright'
+ expect(page).to have_content '@bette.cartwright'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
+ end
+ end
+ end
+
+ it 'unverified signature: user email does not match the committer email' do
+ user_2_key
+
+ visit project_commits_path(project, :'signed-commits')
+
+ within(find('.commit', text: 'signed commit by bette cartwright')) do
+ click_on 'Unverified'
+ within '.popover' do
+ expect(page).to have_content "This commit was signed with a different user's verified signature."
+ expect(page).to have_content 'Bette Cartwright'
+ expect(page).to have_content '@bette.cartwright'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User2.primary_keyid}"
+ end
+ end
+ end
+
+ it 'verified and the gpg user has a gitlab profile' do
+ user_1_key
+
+ visit project_commits_path(project, :'signed-commits')
+
+ within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do
+ click_on 'Verified'
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
+ expect(page).to have_content 'Nannie Bernhard'
+ expect(page).to have_content '@nannie.bernhard'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
+ end
+ end
+ end
+
+ it "verified and the gpg user's profile doesn't exist anymore" do
+ user_1_key
+
+ visit project_commits_path(project, :'signed-commits')
+
+ # wait for the signature to get generated
+ within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do
+ expect(page).to have_content 'Verified'
+ end
+
+ user_1.destroy!
+
+ refresh
+
+ within(find('.commit', text: 'signed and authored commit by nannie bernhard')) do
+ click_on 'Verified'
+ within '.popover' do
+ expect(page).to have_content 'This commit was signed with a verified signature and the committer email is verified to belong to the same user.'
+ expect(page).to have_content 'Nannie Bernhard'
+ expect(page).to have_content 'nannie.bernhard@example.com'
+ expect(page).to have_content "GPG Key ID: #{GpgHelpers::User1.primary_keyid}"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
index f1d0905738b..bf79974b8c6 100644
--- a/spec/features/snippets/notes_on_personal_snippets_spec.rb
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -91,11 +91,7 @@ describe 'Comments on personal snippets', :js do
context 'when editing a note' do
it 'changes the text' do
- open_more_actions_dropdown(snippet_notes[0])
-
- page.within("#notes-list li#note_#{snippet_notes[0].id}") do
- click_on 'Edit comment'
- end
+ find('.js-note-edit').trigger('click')
page.within('.current-note-edit-form') do
fill_in 'note[note]', with: 'new content'
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index a919f5fa20b..d732383a1e1 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -41,7 +41,7 @@ feature 'User creates snippet', :js do
expect(page).to have_content('My Snippet')
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
- expect(link).to match(%r{/uploads/system/temp/\h{32}/banana_sample\.gif\z})
+ expect(link).to match(%r{/uploads/-/system/temp/\h{32}/banana_sample\.gif\z})
visit(link)
expect(page.status_code).to eq(200)
@@ -59,7 +59,7 @@ feature 'User creates snippet', :js do
wait_for_requests
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
- expect(link).to match(%r{/uploads/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
+ expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
visit(link)
expect(page.status_code).to eq(200)
@@ -84,7 +84,7 @@ feature 'User creates snippet', :js do
end
expect(page).to have_content('Hello World!')
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
- expect(link).to match(%r{/uploads/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
+ expect(link).to match(%r{/uploads/-/system/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z})
visit(link)
expect(page.status_code).to eq(200)
diff --git a/spec/features/snippets/user_edits_snippet_spec.rb b/spec/features/snippets/user_edits_snippet_spec.rb
index 26070e508e2..71de6b6bd1c 100644
--- a/spec/features/snippets/user_edits_snippet_spec.rb
+++ b/spec/features/snippets/user_edits_snippet_spec.rb
@@ -33,7 +33,7 @@ feature 'User edits snippet', :js do
wait_for_requests
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
- expect(link).to match(%r{/uploads/system/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z})
+ expect(link).to match(%r{/uploads/-/system/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z})
end
it 'updates the snippet to make it internal' do
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index 4d6fc13557f..d6a6b8fc7d5 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -36,8 +36,8 @@ feature 'Master deletes tag' do
context 'when pre-receive hook fails', js: true do
before do
- allow_any_instance_of(GitHooksService).to receive(:execute)
- .and_raise(GitHooksService::PreReceiveError, 'Do not delete tags')
+ 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
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index c14826df55a..aeb0534b733 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -52,8 +52,8 @@ feature 'Task Lists' do
before do
Warden.test_mode!
- project.team << [user, :master]
- project.team << [user2, :guest]
+ project.add_master(user)
+ project.add_guest(user2)
login_as(user)
end
@@ -181,7 +181,7 @@ feature 'Task Lists' do
project: project, author: user)
end
- it 'renders for note body' do
+ it 'renders for note body', :js do
visit_issue(project, issue)
expect(page).to have_selector('.note ul.task-list', count: 1)
@@ -189,21 +189,20 @@ feature 'Task Lists' do
expect(page).to have_selector('.note ul input[checked]', count: 2)
end
- it 'contains the required selectors' do
+ it 'contains the required selectors', :js do
visit_issue(project, issue)
expect(page).to have_selector('.note .js-task-list-container')
expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox')
- expect(page).to have_selector('.note .js-task-list-container .js-task-list-field')
end
- it 'is only editable by author' do
+ it 'is only editable by author', :js do
visit_issue(project, issue)
expect(page).to have_selector('.js-task-list-container')
- logout(:user)
+ gitlab_sign_out
- login_as(user2)
+ gitlab_sign_in(user2)
visit current_path
expect(page).not_to have_selector('.js-task-list-container')
end
@@ -215,7 +214,7 @@ feature 'Task Lists' do
project: project, author: user)
end
- it 'renders for note body' do
+ it 'renders for note body', :js do
visit_issue(project, issue)
expect(page).to have_selector('.note ul.task-list', count: 1)
@@ -230,7 +229,7 @@ feature 'Task Lists' do
project: project, author: user)
end
- it 'renders for note body' do
+ it 'renders for note body', :js do
visit_issue(project, issue)
expect(page).to have_selector('.note ul.task-list', count: 1)
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 8d12a492feb..47664de469a 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -82,7 +82,7 @@ feature 'Triggers', js: true do
end
describe 'trigger "Take ownership" workflow' do
- before(:each) do
+ before do
create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project)
end
@@ -104,7 +104,7 @@ feature 'Triggers', js: true do
end
describe 'trigger "Revoke" workflow' do
- before(:each) do
+ before do
create(:ci_trigger, owner: user2, project: @project, description: trigger_title)
visit project_settings_ci_cd_path(@project)
end
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 53cad623a35..e1c95590af1 100644
--- a/spec/features/uploads/user_uploads_file_to_note_spec.rb
+++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb
@@ -10,6 +10,7 @@ feature 'User uploads file to note' do
before do
sign_in(user)
visit project_issue_path(project, issue)
+ wait_for_requests
end
context 'before uploading' do
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index ff004d85272..15b89dac572 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -73,7 +73,7 @@ feature 'Users', js: true do
let(:loading_icon) { '.fa.fa-spinner' }
let(:username_input) { 'new_user_username' }
- before(:each) do
+ before do
visit new_user_session_path
click_link 'Register'
end
@@ -104,7 +104,7 @@ feature 'Users', js: true do
end
def errors_on_page(page)
- page.find('#error_explanation').find('ul').all('li').map{ |item| item.text }.join("\n")
+ page.find('#error_explanation').find('ul').all('li').map { |item| item.text }.join("\n")
end
def number_of_errors_on_page(page)
diff --git a/spec/finders/admin/projects_finder_spec.rb b/spec/finders/admin/projects_finder_spec.rb
index 4e367d39cf3..4b67203a0df 100644
--- a/spec/finders/admin/projects_finder_spec.rb
+++ b/spec/finders/admin/projects_finder_spec.rb
@@ -38,6 +38,12 @@ describe Admin::ProjectsFinder do
it { is_expected.to match_array([shared_project, public_project, internal_project, private_project]) }
end
+ context 'with pending delete project' do
+ let!(:pending_delete_project) { create(:project, pending_delete: true) }
+
+ it { is_expected.not_to include(pending_delete_project) }
+ end
+
context 'filter by namespace_id' do
let(:namespace) { create(:namespace) }
let!(:project_in_namespace) { create(:project, namespace: namespace) }
@@ -112,6 +118,12 @@ describe Admin::ProjectsFinder do
it { is_expected.to match_array([archived_project, shared_project, public_project, internal_project, private_project]) }
end
+
+ context 'archived=only' do
+ let(:params) { { archived: 'only' } }
+
+ it { is_expected.to eq([archived_project]) }
+ end
end
context 'filter by personal' do
diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb
index 2d079ea83b4..60ea98e61c7 100644
--- a/spec/finders/contributed_projects_finder_spec.rb
+++ b/spec/finders/contributed_projects_finder_spec.rb
@@ -14,8 +14,8 @@ describe ContributedProjectsFinder do
private_project.add_developer(current_user)
public_project.add_master(source_user)
- create(:event, :pushed, project: public_project, target: public_project, author: source_user)
- create(:event, :pushed, project: private_project, target: private_project, author: source_user)
+ create(:push_event, project: public_project, author: source_user)
+ create(:push_event, project: private_project, author: source_user)
end
describe 'without a current user' do
diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb
index 0c063f6d5ee..3a8a1e7de74 100644
--- a/spec/finders/environments_finder_spec.rb
+++ b/spec/finders/environments_finder_spec.rb
@@ -12,7 +12,7 @@ describe EnvironmentsFinder do
context 'tagged deployment' do
before do
- create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
+ create(:deployment, environment: environment, ref: 'v1.1.0', tag: true, sha: project.commit.id)
end
it 'returns environment when with_tags is set' do
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index db3fcc23475..9f285e28535 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -15,7 +15,7 @@ describe GroupMembersFinder, '#execute' do
result = described_class.new(group).execute
- expect(result.to_a).to eq([member3, member2, member1])
+ expect(result.to_a).to match_array([member3, member2, member1])
end
it 'returns members for nested group', :nested_groups do
@@ -27,6 +27,6 @@ describe GroupMembersFinder, '#execute' do
result = described_class.new(nested_group).execute
- expect(result.to_a).to eq([member4, member3, member1])
+ expect(result.to_a).to match_array([member1, member3, member4])
end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 8769a52863c..47b173dea0a 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -10,10 +10,13 @@ describe IssuesFinder do
set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago) }
set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab') }
set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 1.week.from_now) }
+ set(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) }
+ set(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) }
+ set(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) }
describe '#execute' do
- set(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
- set(:label_link) { create(:label_link, label: label, target: issue2) }
+ let!(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
+ let!(:label_link) { create(:label_link, label: label, target: issue2) }
let(:search_user) { user }
let(:params) { {} }
let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
@@ -26,6 +29,10 @@ describe IssuesFinder do
issue1
issue2
issue3
+
+ award_emoji1
+ award_emoji2
+ award_emoji3
end
context 'scope: all' do
@@ -250,6 +257,34 @@ describe IssuesFinder do
end
end
+ context 'filtering by reaction name' do
+ context 'user searches by "thumbsup" reaction' do
+ let(:params) { { my_reaction_emoji: 'thumbsup' } }
+
+ it 'returns issues that the user thumbsup to' do
+ expect(issues).to contain_exactly(issue1)
+ end
+ end
+
+ context 'user2 searches by "thumbsup" reaction' do
+ let(:search_user) { user2 }
+
+ let(:params) { { my_reaction_emoji: 'thumbsup' } }
+
+ it 'returns issues that the user2 thumbsup to' do
+ expect(issues).to contain_exactly(issue2)
+ end
+ end
+
+ context 'user searches by "thumbsdown" reaction' do
+ let(:params) { { my_reaction_emoji: 'thumbsdown' } }
+
+ it 'returns issues that the user thumbsdown to' do
+ expect(issues).to contain_exactly(issue3)
+ end
+ end
+ end
+
context 'when the user is unauthorized' do
let(:search_user) { nil }
@@ -312,6 +347,20 @@ describe IssuesFinder do
end
end
+ describe '#row_count', :request_store do
+ it 'returns the number of rows for the default state' do
+ finder = described_class.new(user)
+
+ expect(finder.row_count).to eq(3)
+ end
+
+ it 'returns the number of rows for a given state' do
+ finder = described_class.new(user, state: 'closed')
+
+ expect(finder.row_count).to be_zero
+ end
+ end
+
describe '#with_confidentiality_access_check' do
let(:guest) { create(:user) }
set(:authorized_user) { create(:user) }
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
index 300ba8422e8..7bb1f45322e 100644
--- a/spec/finders/members_finder_spec.rb
+++ b/spec/finders/members_finder_spec.rb
@@ -17,6 +17,6 @@ describe MembersFinder, '#execute' do
result = described_class.new(project, user2).execute
- expect(result.to_a).to eq([member3, member2, member1])
+ expect(result.to_a).to match_array([member1, member2, member3])
end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index b54155a6704..95f445e7905 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -108,4 +108,18 @@ describe MergeRequestsFinder do
end
end
end
+
+ describe '#row_count', :request_store do
+ it 'returns the number of rows for the default state' do
+ finder = described_class.new(user)
+
+ expect(finder.row_count).to eq(3)
+ end
+
+ it 'returns the number of rows for a given state' do
+ finder = described_class.new(user, state: 'closed')
+
+ expect(finder.row_count).to eq(1)
+ end
+ end
end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index a5de586e869..0dfe6ba9c32 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -123,6 +123,12 @@ describe ProjectsFinder do
it { is_expected.to match_array([public_project, internal_project, archived_project]) }
end
+ describe 'filter by archived only' do
+ let(:params) { { archived: 'only' } }
+
+ it { is_expected.to eq([archived_project]) }
+ end
+
describe 'filter by archived for backward compatibility' do
let(:params) { { archived: false } }
diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json
index 2f12b671dec..1030f323a1f 100644
--- a/spec/fixtures/api/schemas/entities/merge_request.json
+++ b/spec/fixtures/api/schemas/entities/merge_request.json
@@ -18,6 +18,8 @@
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["integer", "null"] },
"human_total_time_spent": { "type": ["integer", "null"] },
+ "milestone": { "type": ["object", "null"] },
+ "labels": { "type": ["array", "null"] },
"in_progress_merge_commit_sha": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] },
"merge_commit_sha": { "type": ["string", "null"] },
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index ff86437fdd5..e1f62508933 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -8,10 +8,15 @@
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
+ "project_id": { "type": ["integer", "null"] },
"title": { "type": "string" },
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" },
+ "project": {
+ "id": { "type": "integer" },
+ "path": { "type": "string" }
+ },
"labels": {
"type": "array",
"items": {
@@ -34,6 +39,7 @@
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
},
+ "type": { "type": "string" },
"title": { "type": "string" },
"priority": { "type": ["integer", "null"] }
},
diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json
index f6346bd0fb6..c76c6945117 100644
--- a/spec/fixtures/api/schemas/pipeline_schedule.json
+++ b/spec/fixtures/api/schemas/pipeline_schedule.json
@@ -31,6 +31,10 @@
"web_url": { "type": "uri" }
},
"additionalProperties": false
+ },
+ "variables": {
+ "type": ["array", "null"],
+ "items": { "$ref": "pipeline_schedule_variable.json" }
}
},
"required": [
diff --git a/spec/fixtures/api/schemas/pipeline_schedule_variable.json b/spec/fixtures/api/schemas/pipeline_schedule_variable.json
new file mode 100644
index 00000000000..f7ccb2d44a0
--- /dev/null
+++ b/spec/fixtures/api/schemas/pipeline_schedule_variable.json
@@ -0,0 +1,8 @@
+{
+ "type": ["object", "null"],
+ "properties": {
+ "key": { "type": "string" },
+ "value": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json
index bd6bfc03199..8acd9488215 100644
--- a/spec/fixtures/api/schemas/public_api/v4/issues.json
+++ b/spec/fixtures/api/schemas/public_api/v4/issues.json
@@ -78,7 +78,13 @@
"downvotes": { "type": "integer" },
"due_date": { "type": ["date", "null"] },
"confidential": { "type": "boolean" },
- "web_url": { "type": "uri" }
+ "web_url": { "type": "uri" },
+ "time_stats": {
+ "time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["string", "null"] },
+ "human_total_time_spent": { "type": ["string", "null"] }
+ }
},
"required": [
"id", "iid", "project_id", "title", "description",
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 60aa47c1259..31b3f4ba946 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -72,7 +72,13 @@
"user_notes_count": { "type": "integer" },
"should_remove_source_branch": { "type": ["boolean", "null"] },
"force_remove_source_branch": { "type": ["boolean", "null"] },
- "web_url": { "type": "uri" }
+ "web_url": { "type": "uri" },
+ "time_stats": {
+ "time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["string", "null"] },
+ "human_total_time_spent": { "type": ["string", "null"] }
+ }
},
"required": [
"id", "iid", "project_id", "title", "description",
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json
index 6181b3ccc86..e6c1d9c9d84 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/login.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json
@@ -19,6 +19,7 @@
"organization",
"last_sign_in_at",
"confirmed_at",
+ "theme_id",
"color_scheme_id",
"projects_limit",
"current_sign_in_at",
diff --git a/spec/fixtures/emails/ios_default.eml b/spec/fixtures/emails/ios_default.eml
index 8d4d58feb16..fa19475104a 100644
--- a/spec/fixtures/emails/ios_default.eml
+++ b/spec/fixtures/emails/ios_default.eml
@@ -76,7 +76,7 @@ Content-Transfer-Encoding: 7bit
<img src="https://meta-discourse.global.ssl.fastly.net/user_avatar/meta.discourse.org/techapj/45/3281.png" title="techAPJ" style="max-width:100%;" width="45" height="45">
</td>
<td>
- <a href="https://meta.discourse.org/users/techapj" target="_blank" style="text-decoration: none; font-weight: bold; color: #006699;; font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;text-decoration:none;font-weight:bold">techAPJ</a><br>
+ <a href="https://meta.discourse.org/users/techapj" target="_blank" style="text-decoration: none; font-weight: 600; color: #006699;; font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;text-decoration:none;font-weight:bold">techAPJ</a><br>
<span style="text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px">November 28</span>
</td>
</tr>
@@ -94,7 +94,7 @@ Content-Transfer-Encoding: 7bit
<div style="color:#666;">
- <p>To respond, reply to this email or visit <a href="https://meta.discourse.org/t/testing-default-email-replies/22638/3" style="text-decoration: none; font-weight: bold; color: #006699;; color:#666;">https://meta.discourse.org/t/testing-default-email-replies/22638/3</a> in your browser.</p>
+ <p>To respond, reply to this email or visit <a href="https://meta.discourse.org/t/testing-default-email-replies/22638/3" style="text-decoration: none; font-weight: 600; color: #006699;; color:#666;">https://meta.discourse.org/t/testing-default-email-replies/22638/3</a> in your browser.</p>
</div>
<hr style="background-color: #ddd; height: 1px; border: 1px;; background-color: #ddd; height: 1px; border: 1px;">
<h4>Previous Replies</h4>
@@ -106,7 +106,7 @@ Content-Transfer-Encoding: 7bit
<img src="https://meta-discourse.global.ssl.fastly.net/user_avatar/meta.discourse.org/codinghorror/45/5297.png" title="codinghorror" style="max-width:100%;" width="45" height="45">
</td>
<td>
- <a href="https://meta.discourse.org/users/codinghorror" target="_blank" style="text-decoration: none; font-weight: bold; color: #006699;; font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;text-decoration:none;font-weight:bold">codinghorror</a><br>
+ <a href="https://meta.discourse.org/users/codinghorror" target="_blank" style="text-decoration: none; font-weight: 600; color: #006699;; font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;text-decoration:none;font-weight:bold">codinghorror</a><br>
<span style="text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px">November 28</span>
</td>
</tr>
@@ -114,7 +114,7 @@ Content-Transfer-Encoding: 7bit
<td style="padding-top:5px;" colspan="2">
<p style="margin-top:0; border: 0;">We're testing the latest GitHub email processing library which we are integrating now.</p>
-<p style="margin-top:0; border: 0;"><a href="https://github.com/github/email_reply_parser" target="_blank" style="text-decoration: none; font-weight: bold; color: #006699;">https://github.com/github/email_reply_parser</a></p>
+<p style="margin-top:0; border: 0;"><a href="https://github.com/github/email_reply_parser" target="_blank" style="text-decoration: none; font-weight: 600; color: #006699;">https://github.com/github/email_reply_parser</a></p>
<p style="margin-top:0; border: 0;">Go ahead and reply to this topic and I'll reply from various email clients for testing.</p>
</td>
@@ -126,10 +126,10 @@ Content-Transfer-Encoding: 7bit
<hr style="background-color: #ddd; height: 1px; border: 1px;; background-color: #ddd; height: 1px; border: 1px;">
<div style="color:#666;">
-<p>To respond, reply to this email or visit <a href="https://meta.discourse.org/t/testing-default-email-replies/22638/3" style="text-decoration: none; font-weight: bold; color: #006699;; color:#666;">https://meta.discourse.org/t/testing-default-email-replies/22638/3</a> in your browser.</p>
+<p>To respond, reply to this email or visit <a href="https://meta.discourse.org/t/testing-default-email-replies/22638/3" style="text-decoration: none; font-weight: 600; color: #006699;; color:#666;">https://meta.discourse.org/t/testing-default-email-replies/22638/3</a> in your browser.</p>
</div>
<div style="color:#666;">
-<p>To unsubscribe from these emails, visit your <a href="https://meta.discourse.org/my/preferences" style="text-decoration: none; font-weight: bold; color: #006699;; color:#666;">user preferences</a>.</p>
+<p>To unsubscribe from these emails, visit your <a href="https://meta.discourse.org/my/preferences" style="text-decoration: none; font-weight: 600; color: #006699;; color:#666;">user preferences</a>.</p>
</div>
</div>
</div></blockquote></body></html>
diff --git a/spec/fixtures/emails/on_wrote.eml b/spec/fixtures/emails/on_wrote.eml
index feb59bd27bb..af6a4e50a49 100644
--- a/spec/fixtures/emails/on_wrote.eml
+++ b/spec/fixtures/emails/on_wrote.eml
@@ -53,7 +53,7 @@ y
> display: inline-block;
> font-family: FontAwesome;
> font-style: normal;
-> font-weight: normal;
+> font-weight: 400;
> line-height: 1;
> -webkit-font-smoothing: antialiased;
> -moz-osx-font-smoothing: grayscale;
@@ -227,7 +227,7 @@ ding:5px">.fa {
display: inline-block;
font-family: FontAwesome;
font-style: normal;
- font-weight: normal;
+ font-weight: 400;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -274,4 +274,4 @@ ight:bold;color:#006699" target=3D"_blank">user preferences</a>.</p>
</div>
</blockquote></div><br></div></div>
---001a11c34c389e728f0502aa26a0-- \ No newline at end of file
+--001a11c34c389e728f0502aa26a0--
diff --git a/spec/fixtures/encoding/Japanese.md b/spec/fixtures/encoding/Japanese.md
new file mode 100644
index 00000000000..dd469c9f232
--- /dev/null
+++ b/spec/fixtures/encoding/Japanese.md
@@ -0,0 +1,42 @@
++++
+date = "2017-05-21T13:05:07+09:00"
+title = "レイヤ"
+weight = 10
+
++++
+
+## ã“ã®ãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã§æ‰±ã†å†…容
+1. Redactedã«ãŠã‘ã‚‹2D開発ã§ã®ãƒ¬ã‚¤ãƒ¤ã®åŸºæœ¬çš„ãªæ¦‚è¦
+2. スクリーン上ã®ã‚¹ãƒ—ライトã®é †åºä»˜ã‘方法
+
+### Redactedã«ãŠã‘ã‚‹2D開発ã§ã®ãƒ¬ã‚¤ãƒ¤ã®åŸºæœ¬çš„ãªæ¦‚è¦
+2Dã«ãŠã„ã¦ã¯z軸ãŒå­˜åœ¨ã—ãªã„ãŸã‚ã€ã‚·ãƒ¼ãƒ³å†…è¦ç´ ã®æ画順を制御ã™ã‚‹ãŸã‚ã«ã¯ä»£æ›¿ã¨ãªã‚‹ä»•çµ„ã¿ãŒå¿…è¦ã§ã™ã€‚
+Redactedã§ã¯**レイヤ**ã«ãŠã‘ã‚‹**zIndex**属性を制御å¯èƒ½ã«ã™ã‚‹ã“ã¨ã§ã€ã“ã®èª²é¡Œã‚’解決ã—ã¦ã„ã¾ã™ã€‚
+**デフォルトã§ã¯ã€zIndexã¯0ã¨ãªã‚Šã‚ªãƒ–ジェクトã¯ãƒ¬ã‚¤ãƒ¤ã«è¿½åŠ ã•ã‚ŒãŸé †ç•ªã«æç”»ã•ã‚Œã¾ã™ã€‚**
+
+レイヤã«ã¯ã„ãã¤ã‹ã®é‡è¦ãªç‰¹æ€§ãŒã‚ã‚Šã¾ã™ã€‚
+
+* レイヤã«ã¯ãƒ¬ã‚¤ãƒ¤åŒ–ã•ã‚ŒãŸã‚ªãƒ–ジェクトã®ã¿ã‚’å«ã‚ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚(**3Dモデルã¯çµ¶å¯¾ã«è¿½åŠ ã—ãªã„ã§ãã ã•ã„**)
+* レイヤã¯ãƒ¬ã‚¤ãƒ¤åŒ–ã•ã‚ŒãŸã‚ªãƒ–ジェクトã§ã™ã€‚(ã—ãŸãŒã£ã¦ã€ãƒ¬ã‚¤ãƒ¤ã«ã¯ä»–ã®ãƒ¬ã‚¤ãƒ¤ã‚’å«ã‚ã‚‹ã“ã¨ãŒã§ãã¾ã™)
+* レイヤ化ã•ã‚ŒãŸã‚ªãƒ–ジェクトã¯ã€æœ€å¤§ã§1ã¤ã®ãƒ¬ã‚¤ãƒ¤ã«å±žã™ã“ã¨ãŒã§ãã¾ã™ã€‚
+
+レイヤを直接åˆæœŸåŒ–ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“ãŒã€ãã®æ´¾ç”Ÿã‚¯ãƒ©ã‚¹ã¯åˆæœŸåŒ–ã™ã‚‹ã“ã¨ãŒå¯èƒ½ã§ã™ã€‚**Scene2D**ã¨**コンテナ**ã¯ã€**レイヤ**ã‹ã‚‰æ´¾ç”Ÿã™ã‚‹2ã¤ã®ä¸»ãªã‚ªãƒ–ジェクトã§ã™ã€‚ã™ã¹ã¦ã®åˆæœŸåŒ–(createContainerã€instantiateã€...)ã¯ãƒ¬ã‚¤ãƒ¤ä¸Šã§è¡Œã‚ã‚Œã¾ã™ã€‚ã¤ã¾ã‚Šã€2Dã§åˆæœŸåŒ–ã•ã‚Œã‚‹ã™ã¹ã¦ã®ã‚ªãƒ–ジェクトã¯ã€zIndexプロパティをæŒã¤ãƒ¬ã‚¤ãƒ¤åŒ–ã•ã‚ŒãŸã‚ªãƒ–ジェクトã§ã™ã€‚
+
+**zIndexã¯ã‚°ãƒ­ãƒ¼ãƒãƒ«ã§ã¯ã‚ã‚Šã¾ã›ã‚“!**
+
+CSSã¨ã¯ç•°ãªã‚Šã€zIndexã¯ã™ã¹ã¦ã®ã‚ªãƒ–ジェクトã«å¯¾ã—ã¦ã‚°ãƒ­ãƒ¼ãƒãƒ«ã§ã¯ã‚ã‚Šã¾ã›ã‚“。zIndexプロパティã¯è¦ªãƒ¬ã‚¤ãƒ¤ã«å¯¾ã—ã¦ãƒ­ãƒ¼ã‚«ãƒ«ã§ã™ã€‚詳細ã«ã¤ãã¾ã—ã¦ã¯ã€ã‚³ãƒ³ãƒ†ãƒŠãƒãƒ¥ãƒ¼ãƒˆãƒªã‚¢ãƒ«ã§èª¬æ˜Žã—ã¦ã„ã¾ã™ã€‚ [TODO: Link]。
+
+### スクリーン上ã®ã‚¹ãƒ—ライトã®é †åºä»˜ã‘方法
+ã“ã‚Œã¾ã§å­¦ã‚“ã ã“ã¨ã‚’生ã‹ã—ã¦ã€ç”»é¢ã«ã‚¹ãƒ—ライトを表示ã—ã¦ã€zIndexã®è¨­å®šã‚’ã—ã¦ã¿ã¾ã—ょã†ï¼
+
+* ã¾ãšã€æœ€åˆã« (A,B,C) スプライトを生æˆã—ã¾ã™ã€‚
+* スプライトAをシーンã«è¿½åŠ ã—ã¾ã™ï¼ˆzIndex = 0ã€æ¨™æº–色)
+* スプライトBをシーン2ã«è¿½åŠ ã™ã‚‹ã¨ã€**スプライトAã®ä¸Šã«**表示ã•ã‚Œã¾ã™ï¼ˆzIndex = 0ã€èµ¤è‰²ï¼‰
+* 最後ã«ã‚¹ãƒ—ライトCをシーンã«è¿½åŠ ã—ã¾ã™ï¼ˆé’色)ãŒã€ã‚¹ãƒ—ライトã®zIndexã‚’-1ã«è¨­å®šã™ã‚‹ã¨ã€ã‚¹ãƒ—ライトã¯Aã¨Bã®å¾Œå´ã«è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚
+
+{{< code "static/tutorials/layers.html" >}}
+
+### ソースコード全体
+```js
+{{< snippet "static/tutorials/layers.html" >}}
+```
diff --git a/spec/fixtures/fuzzy.po b/spec/fixtures/fuzzy.po
new file mode 100644
index 00000000000..99b7d12b91a
--- /dev/null
+++ b/spec/fixtures/fuzzy.po
@@ -0,0 +1,27 @@
+# Spanish translations for gitlab package.
+# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the gitlab package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab 1.0.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2017-07-12 12:35-0500\n"
+"Language-Team: Spanish\n"
+"Language: es\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"
+"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
+"X-Generator: Poedit 2.0.2\n"
+
+msgid "1 commit"
+msgid_plural "%d commits"
+msgstr[0] "1 cambio"
+msgstr[1] "%d cambios"
+
+#, fuzzy
+msgid "PipelineSchedules|Remove variable row"
+msgstr "Схема"
diff --git a/spec/fixtures/invalid.po b/spec/fixtures/invalid.po
new file mode 100644
index 00000000000..039a56e9fc0
--- /dev/null
+++ b/spec/fixtures/invalid.po
@@ -0,0 +1,25 @@
+# Spanish translations for gitlab package.
+# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the gitlab package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab 1.0.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2017-07-12 12:35-0500\n"
+"Language-Team: Spanish\n"
+"Language: es\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"
+"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
+"X-Generator: Poedit 2.0.2\n"
+
+msgid "%d commit"
+msgid_plural "%d commits"
+msgstr[0] "%d cambio"
+msgstr[1] "%d cambios"
+
+But this doesn't even look like an PO-entry \ No newline at end of file
diff --git a/spec/fixtures/missing_metadata.po b/spec/fixtures/missing_metadata.po
new file mode 100644
index 00000000000..b1999c933f1
--- /dev/null
+++ b/spec/fixtures/missing_metadata.po
@@ -0,0 +1,4 @@
+msgid "1 commit"
+msgid_plural "%d commits"
+msgstr[0] "1 cambio"
+msgstr[1] "%d cambios"
diff --git a/spec/fixtures/missing_plurals.po b/spec/fixtures/missing_plurals.po
new file mode 100644
index 00000000000..09ca0c82718
--- /dev/null
+++ b/spec/fixtures/missing_plurals.po
@@ -0,0 +1,22 @@
+# Spanish translations for gitlab package.
+# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the gitlab package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab 1.0.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2017-07-13 12:10-0500\n"
+"Language-Team: Spanish\n"
+"Language: es\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"
+"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
+"X-Generator: Poedit 2.0.2\n"
+
+msgid "%d commit"
+msgid_plural "%d commits"
+msgstr[0] "%d cambio"
diff --git a/spec/fixtures/multiple_plurals.po b/spec/fixtures/multiple_plurals.po
new file mode 100644
index 00000000000..84b17b13ffa
--- /dev/null
+++ b/spec/fixtures/multiple_plurals.po
@@ -0,0 +1,26 @@
+# Arthur Charron <arthur.charron@hotmail.fr>, 2017. #zanata
+# Huang Tao <htve@outlook.com>, 2017. #zanata
+# Kohei Ota <inductor@kela.jp>, 2017. #zanata
+# Taisuke Inoue <taisuke.inoue.jp@gmail.com>, 2017. #zanata
+# Takuya Noguchi <takninnovationresearch@gmail.com>, 2017. #zanata
+# YANO Tethurou <tetuyano+zana@gmail.com>, 2017. #zanata
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab 1.0.0\n"
+"Report-Msgid-Bugs-To: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"PO-Revision-Date: 2017-08-06 11:23-0400\n"
+"Last-Translator: Taisuke Inoue <taisuke.inoue.jp@gmail.com>\n"
+"Language-Team: Japanese \"Language-Team: Russian (https://translate.zanata.org/"
+"project/view/GitLab)\n"
+"Language: ja\n"
+"X-Generator: Zanata 3.9.6\n"
+"Plural-Forms: nplurals=3; plural=n\n"
+
+msgid "%d commit"
+msgid_plural "%d commits"
+msgstr[0] "%d個ã®ã‚³ãƒŸãƒƒãƒˆ"
+msgstr[1] "%d個ã®ã‚³ãƒŸãƒƒãƒˆ"
+msgstr[2] "missing a variable"
diff --git a/spec/fixtures/newlines.po b/spec/fixtures/newlines.po
new file mode 100644
index 00000000000..f5bc86f39a7
--- /dev/null
+++ b/spec/fixtures/newlines.po
@@ -0,0 +1,48 @@
+# Spanish translations for gitlab package.
+# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the gitlab package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab 1.0.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2017-07-12 12:35-0500\n"
+"Language-Team: Spanish\n"
+"Language: es\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"
+"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
+"X-Generator: Poedit 2.0.2\n"
+
+msgid "1 commit"
+msgid_plural "%d commits"
+msgstr[0] "1 cambio"
+msgstr[1] "%d cambios"
+
+msgid ""
+"You are going to remove %{group_name}.\n"
+"Removed groups CANNOT be restored!\n"
+"Are you ABSOLUTELY sure?"
+msgstr ""
+"Va a eliminar %{group_name}.\n"
+"¡El grupo eliminado NO puede ser restaurado!\n"
+"¿Estás TOTALMENTE seguro?"
+
+msgid "With plural"
+msgid_plural "with plurals"
+msgstr[0] "first"
+msgstr[1] "second"
+msgstr[2] ""
+"with"
+"multiple"
+"lines"
+
+msgid "multiline plural id"
+msgid_plural ""
+"Plural"
+"Id"
+msgstr[0] "first"
+msgstr[1] "second"
diff --git a/spec/fixtures/unescaped_chars.po b/spec/fixtures/unescaped_chars.po
new file mode 100644
index 00000000000..fbafe523fb3
--- /dev/null
+++ b/spec/fixtures/unescaped_chars.po
@@ -0,0 +1,21 @@
+# Spanish translations for gitlab package.
+# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the gitlab package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab 1.0.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2017-07-13 12:10-0500\n"
+"Language-Team: Spanish\n"
+"Language: es\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"
+"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
+"X-Generator: Poedit 2.0.2\n"
+
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr "å°‡è¦æŠŠ %{project_name_with_namespace} 的所有權轉移給å¦ä¸€å€‹äººã€‚真的「100%確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
diff --git a/spec/fixtures/valid.po b/spec/fixtures/valid.po
new file mode 100644
index 00000000000..e43fd5fea15
--- /dev/null
+++ b/spec/fixtures/valid.po
@@ -0,0 +1,1136 @@
+# Spanish translations for gitlab package.
+# Copyright (C) 2017 THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the gitlab package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab 1.0.0\n"
+"Report-Msgid-Bugs-To: \n"
+"PO-Revision-Date: 2017-07-13 12:10-0500\n"
+"Language-Team: Spanish\n"
+"Language: es\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"
+"Last-Translator: Bob Van Landuyt <bob@gitlab.com>\n"
+"X-Generator: Poedit 2.0.2\n"
+
+msgid "%d commit"
+msgid_plural "%d commits"
+msgstr[0] "%d cambio"
+msgstr[1] "%d cambios"
+
+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 cambio adicional ha sido omitido para evitar problemas de rendimiento."
+msgstr[1] "%s cambios adicionales han sido omitidos para evitar problemas de rendimiento."
+
+msgid "%{commit_author_link} committed %{commit_timeago}"
+msgstr "%{commit_author_link} cambió %{commit_timeago}"
+
+msgid "1 pipeline"
+msgid_plural "%d pipelines"
+msgstr[0] "1 pipeline"
+msgstr[1] "%d pipelines"
+
+msgid "A collection of graphs regarding Continuous Integration"
+msgstr "Una colección de gráficos sobre Integración Continua"
+
+msgid "About auto deploy"
+msgstr "Acerca del auto despliegue"
+
+msgid "Active"
+msgstr "Activo"
+
+msgid "Activity"
+msgstr "Actividad"
+
+msgid "Add Changelog"
+msgstr "Agregar Changelog"
+
+msgid "Add Contribution guide"
+msgstr "Agregar guía de contribución"
+
+msgid "Add License"
+msgstr "Agregar Licencia"
+
+msgid "Add an SSH key to your profile to pull or push via SSH."
+msgstr "Agregar una clave SSH a tu perfil para actualizar o enviar a través de SSH."
+
+msgid "Add new directory"
+msgstr "Agregar nuevo directorio"
+
+msgid "Archived project! Repository is read-only"
+msgstr "¡Proyecto archivado! El repositorio es de solo lectura"
+
+msgid "Are you sure you want to delete this pipeline schedule?"
+msgstr "¿Estás seguro que deseas eliminar esta programación del pipeline?"
+
+msgid "Attach a file by drag &amp; drop or %{upload_link}"
+msgstr "Adjunte un archivo arrastrando &amp; soltando o %{upload_link}"
+
+msgid "Branch"
+msgid_plural "Branches"
+msgstr[0] "Rama"
+msgstr[1] "Ramas"
+
+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 rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}"
+
+msgid "BranchSwitcherPlaceholder|Search branches"
+msgstr "Buscar ramas"
+
+msgid "BranchSwitcherTitle|Switch branch"
+msgstr "Cambiar rama"
+
+msgid "Branches"
+msgstr "Ramas"
+
+msgid "Browse Directory"
+msgstr "Examinar directorio"
+
+msgid "Browse File"
+msgstr "Examinar archivo"
+
+msgid "Browse Files"
+msgstr "Examinar archivos"
+
+msgid "Browse files"
+msgstr "Examinar archivos"
+
+msgid "ByAuthor|by"
+msgstr "por"
+
+msgid "CI configuration"
+msgstr "Configuración de CI"
+
+msgid "Cancel"
+msgstr "Cancelar"
+
+msgid "ChangeTypeActionLabel|Pick into branch"
+msgstr "Escoger en la rama"
+
+msgid "ChangeTypeActionLabel|Revert in branch"
+msgstr "Revertir en la rama"
+
+msgid "ChangeTypeAction|Cherry-pick"
+msgstr "Cherry-pick"
+
+msgid "ChangeTypeAction|Revert"
+msgstr "Revertir"
+
+msgid "Changelog"
+msgstr "Changelog"
+
+msgid "Charts"
+msgstr "Gráficos"
+
+msgid "Cherry-pick this commit"
+msgstr "Escoger este cambio"
+
+msgid "Cherry-pick this merge request"
+msgstr "Escoger esta solicitud de fusión"
+
+msgid "CiStatusLabel|canceled"
+msgstr "cancelado"
+
+msgid "CiStatusLabel|created"
+msgstr "creado"
+
+msgid "CiStatusLabel|failed"
+msgstr "fallido"
+
+msgid "CiStatusLabel|manual action"
+msgstr "acción manual"
+
+msgid "CiStatusLabel|passed"
+msgstr "pasó"
+
+msgid "CiStatusLabel|passed with warnings"
+msgstr "pasó con advertencias"
+
+msgid "CiStatusLabel|pending"
+msgstr "pendiente"
+
+msgid "CiStatusLabel|skipped"
+msgstr "omitido"
+
+msgid "CiStatusLabel|waiting for manual action"
+msgstr "esperando acción manual"
+
+msgid "CiStatusText|blocked"
+msgstr "bloqueado"
+
+msgid "CiStatusText|canceled"
+msgstr "cancelado"
+
+msgid "CiStatusText|created"
+msgstr "creado"
+
+msgid "CiStatusText|failed"
+msgstr "fallado"
+
+msgid "CiStatusText|manual"
+msgstr "manual"
+
+msgid "CiStatusText|passed"
+msgstr "pasó"
+
+msgid "CiStatusText|pending"
+msgstr "pendiente"
+
+msgid "CiStatusText|skipped"
+msgstr "omitido"
+
+msgid "CiStatus|running"
+msgstr "en ejecución"
+
+msgid "Commit"
+msgid_plural "Commits"
+msgstr[0] "Cambio"
+msgstr[1] "Cambios"
+
+msgid "Commit duration in minutes for last 30 commits"
+msgstr "Duración de los cambios en minutos para los últimos 30"
+
+msgid "Commit message"
+msgstr "Mensaje del cambio"
+
+msgid "CommitBoxTitle|Commit"
+msgstr "Cambio"
+
+msgid "CommitMessage|Add %{file_name}"
+msgstr "Agregar %{file_name}"
+
+msgid "Commits"
+msgstr "Cambios"
+
+msgid "Commits feed"
+msgstr "Feed de cambios"
+
+msgid "Commits|History"
+msgstr "Historial"
+
+msgid "Committed by"
+msgstr "Enviado por"
+
+msgid "Compare"
+msgstr "Comparar"
+
+msgid "Contribution guide"
+msgstr "Guía de contribución"
+
+msgid "Contributors"
+msgstr "Contribuidores"
+
+msgid "Copy URL to clipboard"
+msgstr "Copiar URL al portapapeles"
+
+msgid "Copy commit SHA to clipboard"
+msgstr "Copiar SHA del cambio al portapapeles"
+
+msgid "Create New Directory"
+msgstr "Crear Nuevo Directorio"
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr "Crear un token de acceso personal en tu cuenta para actualizar o enviar a través de %{protocol}."
+
+msgid "Create directory"
+msgstr "Crear directorio"
+
+msgid "Create empty bare repository"
+msgstr "Crear repositorio vacío"
+
+msgid "Create merge request"
+msgstr "Crear solicitud de fusión"
+
+msgid "Create new..."
+msgstr "Crear nuevo..."
+
+msgid "CreateNewFork|Fork"
+msgstr "Bifurcar"
+
+msgid "CreateTag|Tag"
+msgstr "Etiqueta"
+
+msgid "CreateTokenToCloneLink|create a personal access token"
+msgstr "crear un token de acceso personal"
+
+msgid "Cron Timezone"
+msgstr "Zona horaria del Cron"
+
+msgid "Cron syntax"
+msgstr "Sintaxis de Cron"
+
+msgid "Custom notification events"
+msgstr "Eventos de notificaciones personalizadas"
+
+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 "Los niveles de notificación personalizados son los mismos que los niveles participantes. Con los niveles de notificación personalizados, también recibirá notificaciones para eventos seleccionados. Para obtener más información, consulte %{notification_link}."
+
+msgid "Cycle Analytics"
+msgstr "Cycle Analytics"
+
+msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
+msgstr "Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."
+
+msgid "CycleAnalyticsStage|Code"
+msgstr "Código"
+
+msgid "CycleAnalyticsStage|Issue"
+msgstr "Incidencia"
+
+msgid "CycleAnalyticsStage|Plan"
+msgstr "Planificación"
+
+msgid "CycleAnalyticsStage|Production"
+msgstr "Producción"
+
+msgid "CycleAnalyticsStage|Review"
+msgstr "Revisión"
+
+msgid "CycleAnalyticsStage|Staging"
+msgstr "Puesta en escena"
+
+msgid "CycleAnalyticsStage|Test"
+msgstr "Pruebas"
+
+msgid "Define a custom pattern with cron syntax"
+msgstr "Definir un patrón personalizado con la sintaxis de cron"
+
+msgid "Delete"
+msgstr "Eliminar"
+
+msgid "Deploy"
+msgid_plural "Deploys"
+msgstr[0] "Despliegue"
+msgstr[1] "Despliegues"
+
+msgid "Description"
+msgstr "Descripción"
+
+msgid "Directory name"
+msgstr "Nombre del directorio"
+
+msgid "Don't show again"
+msgstr "No mostrar de nuevo"
+
+msgid "Download"
+msgstr "Descargar"
+
+msgid "Download tar"
+msgstr "Descargar tar"
+
+msgid "Download tar.bz2"
+msgstr "Descargar tar.bz2"
+
+msgid "Download tar.gz"
+msgstr "Descargar tar.gz"
+
+msgid "Download zip"
+msgstr "Descargar zip"
+
+msgid "DownloadArtifacts|Download"
+msgstr "Descargar"
+
+msgid "DownloadCommit|Email Patches"
+msgstr "Parches por correo electrónico"
+
+msgid "DownloadCommit|Plain Diff"
+msgstr "Diferencias en texto plano"
+
+msgid "DownloadSource|Download"
+msgstr "Descargar"
+
+msgid "Edit"
+msgstr "Editar"
+
+msgid "Edit Pipeline Schedule %{id}"
+msgstr "Editar Programación del Pipeline %{id}"
+
+msgid "Every day (at 4:00am)"
+msgstr "Todos los días (a las 4:00 am)"
+
+msgid "Every month (on the 1st at 4:00am)"
+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 "Failed to change the owner"
+msgstr "Error al cambiar el propietario"
+
+msgid "Failed to remove the pipeline schedule"
+msgstr "Error al eliminar la programación del pipeline"
+
+msgid "Files"
+msgstr "Archivos"
+
+msgid "Filter by commit message"
+msgstr "Filtrar por mensaje del cambio"
+
+msgid "Find by path"
+msgstr "Buscar por ruta"
+
+msgid "Find file"
+msgstr "Buscar archivo"
+
+msgid "FirstPushedBy|First"
+msgstr "Primer"
+
+msgid "FirstPushedBy|pushed by"
+msgstr "enviado por"
+
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] "Bifurcación"
+msgstr[1] "Bifurcaciones"
+
+msgid "ForkedFromProjectPath|Forked from"
+msgstr "Bifurcado de"
+
+msgid "From issue creation until deploy to production"
+msgstr "Desde la creación de la incidencia hasta el despliegue a producción"
+
+msgid "From merge request merge until deploy to production"
+msgstr "Desde la integración de la solicitud de fusión hasta el despliegue a producción"
+
+msgid "Go to your fork"
+msgstr "Ir a tu bifurcación"
+
+msgid "GoToYourFork|Fork"
+msgstr "Bifurcación"
+
+msgid "Home"
+msgstr "Inicio"
+
+msgid "Housekeeping successfully started"
+msgstr "Servicio de limpieza iniciado con éxito"
+
+msgid "Import repository"
+msgstr "Importar repositorio"
+
+msgid "Interval Pattern"
+msgstr "Patrón de intervalo"
+
+msgid "Introducing Cycle Analytics"
+msgstr "Introducción a Cycle Analytics"
+
+msgid "Jobs for last month"
+msgstr "Trabajos del mes pasado"
+
+msgid "Jobs for last week"
+msgstr "Trabajos de la semana pasada"
+
+msgid "Jobs for last year"
+msgstr "Trabajos del año pasado"
+
+msgid "LFSStatus|Disabled"
+msgstr "Deshabilitado"
+
+msgid "LFSStatus|Enabled"
+msgstr "Habilitado"
+
+msgid "Last %d day"
+msgid_plural "Last %d days"
+msgstr[0] "Último %d día"
+msgstr[1] "Últimos %d días"
+
+msgid "Last Pipeline"
+msgstr "Último Pipeline"
+
+msgid "Last Update"
+msgstr "Última actualización"
+
+msgid "Last commit"
+msgstr "Último cambio"
+
+msgid "Learn more in the"
+msgstr "Más información en la"
+
+msgid "Learn more in the|pipeline schedules documentation"
+msgstr "documentación sobre la programación de pipelines"
+
+msgid "Leave group"
+msgstr "Abandonar grupo"
+
+msgid "Leave project"
+msgstr "Abandonar proyecto"
+
+msgid "Limited to showing %d event at most"
+msgid_plural "Limited to showing %d events at most"
+msgstr[0] "Limitado a mostrar máximo %d evento"
+msgstr[1] "Limitado a mostrar máximo %d eventos"
+
+msgid "Median"
+msgstr "Mediana"
+
+msgid "MissingSSHKeyWarningLink|add an SSH key"
+msgstr "agregar una clave SSH"
+
+msgid "New Issue"
+msgid_plural "New Issues"
+msgstr[0] "Nueva incidencia"
+msgstr[1] "Nuevas incidencias"
+
+msgid "New Pipeline Schedule"
+msgstr "Nueva Programación del Pipeline"
+
+msgid "New branch"
+msgstr "Nueva rama"
+
+msgid "New directory"
+msgstr "Nuevo directorio"
+
+msgid "New file"
+msgstr "Nuevo archivo"
+
+msgid "New issue"
+msgstr "Nueva incidencia"
+
+msgid "New merge request"
+msgstr "Nueva solicitud de fusión"
+
+msgid "New schedule"
+msgstr "Nueva programación"
+
+msgid "New snippet"
+msgstr "Nuevo fragmento de código"
+
+msgid "New tag"
+msgstr "Nueva etiqueta"
+
+msgid "No repository"
+msgstr "No hay repositorio"
+
+msgid "No schedules"
+msgstr "No hay programaciones"
+
+msgid "Not available"
+msgstr "No disponible"
+
+msgid "Not enough data"
+msgstr "No hay suficientes datos"
+
+msgid "Notification events"
+msgstr "Eventos de notificación"
+
+msgid "NotificationEvent|Close issue"
+msgstr "Cerrar incidencia"
+
+msgid "NotificationEvent|Close merge request"
+msgstr "Cerrar solicitud de fusión"
+
+msgid "NotificationEvent|Failed pipeline"
+msgstr "Pipeline fallido"
+
+msgid "NotificationEvent|Merge merge request"
+msgstr "Integrar solicitud de fusión"
+
+msgid "NotificationEvent|New issue"
+msgstr "Nueva incidencia"
+
+msgid "NotificationEvent|New merge request"
+msgstr "Nueva solicitud de fusión"
+
+msgid "NotificationEvent|New note"
+msgstr "Nueva nota"
+
+msgid "NotificationEvent|Reassign issue"
+msgstr "Reasignar incidencia"
+
+msgid "NotificationEvent|Reassign merge request"
+msgstr "Reasignar solicitud de fusión"
+
+msgid "NotificationEvent|Reopen issue"
+msgstr "Reabrir incidencia"
+
+msgid "NotificationEvent|Successful pipeline"
+msgstr "Pipeline exitoso"
+
+msgid "NotificationLevel|Custom"
+msgstr "Personalizado"
+
+msgid "NotificationLevel|Disabled"
+msgstr "Deshabilitado"
+
+msgid "NotificationLevel|Global"
+msgstr "Global"
+
+msgid "NotificationLevel|On mention"
+msgstr "Cuando me mencionan"
+
+msgid "NotificationLevel|Participate"
+msgstr "Participación"
+
+msgid "NotificationLevel|Watch"
+msgstr "Vigilancia"
+
+msgid "OfSearchInADropdown|Filter"
+msgstr "Filtrar"
+
+msgid "OpenedNDaysAgo|Opened"
+msgstr "Abierto"
+
+msgid "Options"
+msgstr "Opciones"
+
+msgid "Owner"
+msgstr "Propietario"
+
+msgid "Pipeline"
+msgstr "Pipeline"
+
+msgid "Pipeline Health"
+msgstr "Estado del Pipeline"
+
+msgid "Pipeline Schedule"
+msgstr "Programación del Pipeline"
+
+msgid "Pipeline Schedules"
+msgstr "Programaciones de los Pipelines"
+
+msgid "PipelineCharts|Failed:"
+msgstr "Fallidos:"
+
+msgid "PipelineCharts|Overall statistics"
+msgstr "Estadísticas generales"
+
+msgid "PipelineCharts|Success ratio:"
+msgstr "Ratio de éxito"
+
+msgid "PipelineCharts|Successful:"
+msgstr "Exitosos:"
+
+msgid "PipelineCharts|Total:"
+msgstr "Total:"
+
+msgid "PipelineSchedules|Activated"
+msgstr "Activado"
+
+msgid "PipelineSchedules|Active"
+msgstr "Activos"
+
+msgid "PipelineSchedules|All"
+msgstr "Todos"
+
+msgid "PipelineSchedules|Inactive"
+msgstr "Inactivos"
+
+msgid "PipelineSchedules|Input variable key"
+msgstr "Ingrese nombre de clave"
+
+msgid "PipelineSchedules|Input variable value"
+msgstr "Ingrese el valor de la variable"
+
+msgid "PipelineSchedules|Next Run"
+msgstr "Próxima Ejecución"
+
+msgid "PipelineSchedules|None"
+msgstr "Ninguno"
+
+msgid "PipelineSchedules|Provide a short description for this pipeline"
+msgstr "Proporcione una descripción breve para este pipeline"
+
+msgid "PipelineSchedules|Remove variable row"
+msgstr "Eliminar fila de variable"
+
+msgid "PipelineSchedules|Take ownership"
+msgstr "Tomar posesión"
+
+msgid "PipelineSchedules|Target"
+msgstr "Destino"
+
+msgid "PipelineSchedules|Variables"
+msgstr "Variables"
+
+msgid "PipelineSheduleIntervalPattern|Custom"
+msgstr "Personalizado"
+
+msgid "Pipelines"
+msgstr "Pipelines"
+
+msgid "Pipelines charts"
+msgstr "Gráficos de los pipelines"
+
+msgid "Pipeline|all"
+msgstr "todos"
+
+msgid "Pipeline|success"
+msgstr "exitósos"
+
+msgid "Pipeline|with stage"
+msgstr "con etapa"
+
+msgid "Pipeline|with stages"
+msgstr "con etapas"
+
+msgid "Project '%{project_name}' queued for deletion."
+msgstr "Proyecto ‘%{project_name}’ en cola para eliminación."
+
+msgid "Project '%{project_name}' was successfully created."
+msgstr "Proyecto ‘%{project_name}’ fue creado satisfactoriamente."
+
+msgid "Project '%{project_name}' was successfully updated."
+msgstr "Proyecto ‘%{project_name}’ fue actualizado satisfactoriamente."
+
+msgid "Project '%{project_name}' will be deleted."
+msgstr "Proyecto ‘%{project_name}’ será eliminado."
+
+msgid "Project access must be granted explicitly to each user."
+msgstr "El acceso al proyecto debe concederse explícitamente a cada usuario."
+
+msgid "Project export could not be deleted."
+msgstr "No se pudo eliminar la exportación del proyecto."
+
+msgid "Project export has been deleted."
+msgstr "La exportación del proyecto ha sido eliminada."
+
+msgid "Project export link has expired. Please generate a new export from your project settings."
+msgstr "El enlace de exportación del proyecto ha caducado. Por favor, genera una nueva exportación desde la configuración del proyecto."
+
+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 "ProjectFeature|Disabled"
+msgstr "Deshabilitada"
+
+msgid "ProjectFeature|Everyone with access"
+msgstr "Todos con acceso"
+
+msgid "ProjectFeature|Only team members"
+msgstr "Solo miembros del equipo"
+
+msgid "ProjectFileTree|Name"
+msgstr "Nombre"
+
+msgid "ProjectLastActivity|Never"
+msgstr "Nunca"
+
+msgid "ProjectLifecycle|Stage"
+msgstr "Etapa"
+
+msgid "ProjectNetworkGraph|Graph"
+msgstr "Historial gráfico"
+
+msgid "Read more"
+msgstr "Leer más"
+
+msgid "Readme"
+msgstr "Léeme"
+
+msgid "RefSwitcher|Branches"
+msgstr "Ramas"
+
+msgid "RefSwitcher|Tags"
+msgstr "Etiquetas"
+
+msgid "Related Commits"
+msgstr "Cambios Relacionados"
+
+msgid "Related Deployed Jobs"
+msgstr "Trabajos Desplegados Relacionados"
+
+msgid "Related Issues"
+msgstr "Incidencias Relacionadas"
+
+msgid "Related Jobs"
+msgstr "Trabajos Relacionados"
+
+msgid "Related Merge Requests"
+msgstr "Solicitudes de fusión Relacionadas"
+
+msgid "Related Merged Requests"
+msgstr "Solicitudes de fusión Relacionadas"
+
+msgid "Remind later"
+msgstr "Recordar después"
+
+msgid "Remove project"
+msgstr "Eliminar proyecto"
+
+msgid "Request Access"
+msgstr "Solicitar acceso"
+
+msgid "Revert this commit"
+msgstr "Revertir este cambio"
+
+msgid "Revert this merge request"
+msgstr "Revertir esta solicitud de fusión"
+
+msgid "Save pipeline schedule"
+msgstr "Guardar programación del pipeline"
+
+msgid "Schedule a new pipeline"
+msgstr "Programar un nuevo pipeline"
+
+msgid "Scheduling Pipelines"
+msgstr "Programación de Pipelines"
+
+msgid "Search branches and tags"
+msgstr "Buscar ramas y etiquetas"
+
+msgid "Select Archive Format"
+msgstr "Seleccionar formato de archivo"
+
+msgid "Select a timezone"
+msgstr "Selecciona una zona horaria"
+
+msgid "Select target branch"
+msgstr "Selecciona una rama de destino"
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr "Establezca una contraseña en su cuenta para actualizar o enviar a través de %{protocol}."
+
+msgid "Set up CI"
+msgstr "Configurar CI"
+
+msgid "Set up Koding"
+msgstr "Configurar Koding"
+
+msgid "Set up auto deploy"
+msgstr "Configurar auto despliegue"
+
+msgid "SetPasswordToCloneLink|set a password"
+msgstr "establecer una contraseña"
+
+msgid "Showing %d event"
+msgid_plural "Showing %d events"
+msgstr[0] "Mostrando %d evento"
+msgstr[1] "Mostrando %d eventos"
+
+msgid "Source code"
+msgstr "Código fuente"
+
+msgid "StarProject|Star"
+msgstr "Destacar"
+
+msgid "Start a %{new_merge_request} with these changes"
+msgstr "Iniciar una %{new_merge_request} con estos cambios"
+
+msgid "Switch branch/tag"
+msgstr "Cambiar rama/etiqueta"
+
+msgid "Tag"
+msgid_plural "Tags"
+msgstr[0] "Etiqueta"
+msgstr[1] "Etiquetas"
+
+msgid "Tags"
+msgstr "Etiquetas"
+
+msgid "Target Branch"
+msgstr "Rama de destino"
+
+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 "La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."
+
+msgid "The collection of events added to the data gathered for that stage."
+msgstr "La colección de eventos agregados a los datos recopilados para esa etapa."
+
+msgid "The fork relationship has been removed."
+msgstr "La relación con la bifurcación se ha eliminado."
+
+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 "La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."
+
+msgid "The phase of the development lifecycle."
+msgstr "La etapa del ciclo de vida de desarrollo."
+
+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 "La programación de pipelines ejecuta pipelines en el futuro, repetidamente, para ramas o etiquetas específicas. Los pipelines programados heredarán acceso limitado al proyecto basado en su usuario asociado."
+
+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 "La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."
+
+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 "La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."
+
+msgid "The project can be accessed by any logged in user."
+msgstr "El proyecto puede ser accedido por cualquier usuario conectado."
+
+msgid "The project can be accessed without any authentication."
+msgstr "El proyecto puede accederse sin ninguna autenticación."
+
+msgid "The repository for this project does not exist."
+msgstr "El repositorio para este proyecto no existe."
+
+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 "La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."
+
+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 "La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."
+
+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 "La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."
+
+msgid "The time taken by each data entry gathered by that stage."
+msgstr "El tiempo utilizado por cada entrada de datos obtenido por esa etapa."
+
+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 "El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."
+
+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."
+
+msgid "Time before an issue gets scheduled"
+msgstr "Tiempo antes de que una incidencia sea programada"
+
+msgid "Time before an issue starts implementation"
+msgstr "Tiempo antes de que empieze la implementación de una incidencia"
+
+msgid "Time between merge request creation and merge/close"
+msgstr "Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"
+
+msgid "Time until first merge request"
+msgstr "Tiempo hasta la primera solicitud de fusión"
+
+msgid "Timeago|%s days ago"
+msgstr "hace %s días"
+
+msgid "Timeago|%s days remaining"
+msgstr "%s días restantes"
+
+msgid "Timeago|%s hours remaining"
+msgstr "%s horas restantes"
+
+msgid "Timeago|%s minutes ago"
+msgstr "hace %s minutos"
+
+msgid "Timeago|%s minutes remaining"
+msgstr "%s minutos restantes"
+
+msgid "Timeago|%s months ago"
+msgstr "hace %s meses"
+
+msgid "Timeago|%s months remaining"
+msgstr "%s meses restantes"
+
+msgid "Timeago|%s seconds remaining"
+msgstr "%s segundos restantes"
+
+msgid "Timeago|%s weeks ago"
+msgstr "hace %s semanas"
+
+msgid "Timeago|%s weeks remaining"
+msgstr "%s semanas restantes"
+
+msgid "Timeago|%s years ago"
+msgstr "hace %s años"
+
+msgid "Timeago|%s years remaining"
+msgstr "%s años restantes"
+
+msgid "Timeago|1 day remaining"
+msgstr "1 día restante"
+
+msgid "Timeago|1 hour remaining"
+msgstr "1 hora restante"
+
+msgid "Timeago|1 minute remaining"
+msgstr "1 minuto restante"
+
+msgid "Timeago|1 month remaining"
+msgstr "1 mes restante"
+
+msgid "Timeago|1 week remaining"
+msgstr "1 semana restante"
+
+msgid "Timeago|1 year remaining"
+msgstr "1 año restante"
+
+msgid "Timeago|Past due"
+msgstr "Atrasado"
+
+msgid "Timeago|a day ago"
+msgstr "hace un día"
+
+msgid "Timeago|a month ago"
+msgstr "hace un mes"
+
+msgid "Timeago|a week ago"
+msgstr "hace una semana"
+
+msgid "Timeago|a while"
+msgstr "hace un momento"
+
+msgid "Timeago|a year ago"
+msgstr "hace un año"
+
+msgid "Timeago|about %s hours ago"
+msgstr "hace alrededor de %s horas"
+
+msgid "Timeago|about a minute ago"
+msgstr "hace alrededor de 1 minuto"
+
+msgid "Timeago|about an hour ago"
+msgstr "hace alrededor de 1 hora"
+
+msgid "Timeago|in %s days"
+msgstr "en %s días"
+
+msgid "Timeago|in %s hours"
+msgstr "en %s horas"
+
+msgid "Timeago|in %s minutes"
+msgstr "en %s minutos"
+
+msgid "Timeago|in %s months"
+msgstr "en %s meses"
+
+msgid "Timeago|in %s seconds"
+msgstr "en %s segundos"
+
+msgid "Timeago|in %s weeks"
+msgstr "en %s semanas"
+
+msgid "Timeago|in %s years"
+msgstr "en %s años"
+
+msgid "Timeago|in 1 day"
+msgstr "en 1 día"
+
+msgid "Timeago|in 1 hour"
+msgstr "en 1 hora"
+
+msgid "Timeago|in 1 minute"
+msgstr "en 1 minuto"
+
+msgid "Timeago|in 1 month"
+msgstr "en 1 mes"
+
+msgid "Timeago|in 1 week"
+msgstr "en 1 semana"
+
+msgid "Timeago|in 1 year"
+msgstr "en 1 año"
+
+msgid "Timeago|less than a minute ago"
+msgstr "hace menos de 1 minuto"
+
+msgid "Time|hr"
+msgid_plural "Time|hrs"
+msgstr[0] "hr"
+msgstr[1] "hrs"
+
+msgid "Time|min"
+msgid_plural "Time|mins"
+msgstr[0] "min"
+msgstr[1] "mins"
+
+msgid "Time|s"
+msgstr "s"
+
+msgid "Total Time"
+msgstr "Tiempo Total"
+
+msgid "Total test time for all commits/merges"
+msgstr "Tiempo total de pruebas para todos los cambios o integraciones"
+
+msgid "Unstar"
+msgstr "No Destacar"
+
+msgid "Upload New File"
+msgstr "Subir nuevo archivo"
+
+msgid "Upload file"
+msgstr "Subir archivo"
+
+msgid "UploadLink|click to upload"
+msgstr "Hacer clic para subir"
+
+msgid "Use your global notification setting"
+msgstr "Utiliza tu configuración de notificación global"
+
+msgid "View open merge request"
+msgstr "Ver solicitud de fusión abierta"
+
+msgid "VisibilityLevel|Internal"
+msgstr "Interno"
+
+msgid "VisibilityLevel|Private"
+msgstr "Privado"
+
+msgid "VisibilityLevel|Public"
+msgstr "Público"
+
+msgid "VisibilityLevel|Unknown"
+msgstr "Desconocido"
+
+msgid "Want to see the data? Please ask an administrator for access."
+msgstr "¿Quieres ver los datos? Por favor pide acceso al administrador."
+
+msgid "We don't have enough data to show this stage."
+msgstr "No hay suficientes datos para mostrar en esta etapa."
+
+msgid "Withdraw Access Request"
+msgstr "Retirar Solicitud de Acceso"
+
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Va a eliminar %{group_name}. ¡El grupo eliminado NO puede ser restaurado! ¿Estás TOTALMENTE seguro?"
+
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr "Va a eliminar %{project_name_with_namespace}. ¡El proyecto eliminado NO puede ser restaurado! ¿Estás TOTALMENTE seguro?"
+
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr "Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?"
+
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr "Vas a transferir %{project_name_with_namespace} a otro propietario. ¿Estás TOTALMENTE seguro?"
+
+msgid "You can only add files when you are on a branch"
+msgstr "Solo puedes agregar archivos cuando estás en una rama"
+
+msgid "You have reached your project limit"
+msgstr "Has alcanzado el límite de tu proyecto"
+
+msgid "You must sign in to star a project"
+msgstr "Debes iniciar sesión para destacar un proyecto"
+
+msgid "You need permission."
+msgstr "Necesitas permisos."
+
+msgid "You will not get any notifications via email"
+msgstr "No recibirás ninguna notificación por correo electrónico"
+
+msgid "You will only receive notifications for the events you choose"
+msgstr "Solo recibirás notificaciones de los eventos que elijas"
+
+msgid "You will only receive notifications for threads you have participated in"
+msgstr "Solo recibirás notificaciones de los temas en los que has participado"
+
+msgid "You will receive notifications for any activity"
+msgstr "Recibirás notificaciones por cualquier actividad"
+
+msgid "You will receive notifications only for comments in which you were @mentioned"
+msgstr "Recibirás notificaciones solo para los comentarios en los que se te mencionó"
+
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr "No podrás actualizar o enviar código al proyecto a través de %{protocol} hasta que %{set_password_link} en tu cuenta"
+
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr "No podrás actualizar o enviar código al proyecto a través de SSH hasta que %{add_ssh_key_link} en su perfil"
+
+msgid "Your name"
+msgstr "Tu nombre"
+
+msgid "day"
+msgid_plural "days"
+msgstr[0] "día"
+msgstr[1] "días"
+
+msgid "new merge request"
+msgstr "nueva solicitud de fusión"
+
+msgid "notification emails"
+msgstr "correos electrónicos de notificación"
+
+msgid "parent"
+msgid_plural "parents"
+msgstr[0] "padre"
+msgstr[1] "padres"
diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb
new file mode 100644
index 00000000000..b6d892548ef
--- /dev/null
+++ b/spec/helpers/auto_devops_helper_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe AutoDevopsHelper do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+
+ describe '.show_auto_devops_callout?' do
+ let(:allowed) { true }
+
+ before do
+ allow(helper).to receive(:can?).with(user, :admin_pipeline, project) { allowed }
+ allow(helper).to receive(:current_user) { user }
+ end
+
+ subject { helper.show_auto_devops_callout?(project) }
+
+ context 'when all conditions are met' do
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when dismissed' do
+ before do
+ helper.request.cookies[:auto_devops_settings_dismissed] = 'true'
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when user cannot admin project' do
+ let(:allowed) { false }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is enabled system-wide' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is explicitly enabled for project' do
+ before do
+ project.create_auto_devops!(enabled: true)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is explicitly disabled for project' do
+ before do
+ project.create_auto_devops!(enabled: false)
+ 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 d16fcf21e45..4632c679972 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -28,7 +28,7 @@ describe AvatarsHelper do
it 'displays user avatar' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
- class: 'avatar has-tooltip s16 lazy',
+ class: 'avatar s16 has-tooltip lazy',
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: avatar_icon(user, 16) }
@@ -41,7 +41,7 @@ describe AvatarsHelper do
it 'uses provided css_class' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
- class: "avatar has-tooltip s16 #{options[:css_class]} lazy",
+ class: "avatar s16 #{options[:css_class]} has-tooltip lazy",
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: avatar_icon(user, 16) }
@@ -55,7 +55,7 @@ describe AvatarsHelper do
it 'uses provided size' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
- class: "avatar has-tooltip s#{options[:size]} lazy",
+ class: "avatar s#{options[:size]} has-tooltip lazy",
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: avatar_icon(user, options[:size]) }
@@ -69,7 +69,7 @@ describe AvatarsHelper do
it 'uses provided url' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
- class: 'avatar has-tooltip s16 lazy',
+ class: 'avatar s16 has-tooltip lazy',
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: options[:url] }
@@ -77,6 +77,36 @@ describe AvatarsHelper do
end
end
+ context 'with has_tooltip parameter' do
+ context 'with has_tooltip set to true' 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',
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body', src: avatar_icon(user, 16) }
+ )
+ end
+ end
+
+ context 'with has_tooltip set to false' 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',
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { src: avatar_icon(user, 16) }
+ )
+ end
+ end
+ end
+
context 'with user_name parameter' do
let(:options) { { user_name: 'Tinky Winky', user_email: 'no@f.un' } }
@@ -86,7 +116,7 @@ describe AvatarsHelper do
it 'prefers user parameter' do
is_expected.to eq image_tag(
LazyImageTagHelper.placeholder_image,
- class: 'avatar has-tooltip s16 lazy',
+ class: 'avatar s16 has-tooltip lazy',
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: avatar_icon(user, 16) }
@@ -97,7 +127,7 @@ describe AvatarsHelper do
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 has-tooltip s16 lazy',
+ class: 'avatar s16 has-tooltip lazy',
alt: "#{options[:user_name]}'s avatar",
title: options[:user_name],
data: { container: 'body', src: avatar_icon(options[:user_email], 16) }
diff --git a/spec/helpers/blame_helper_spec.rb b/spec/helpers/blame_helper_spec.rb
index b4368516d83..722d21c566f 100644
--- a/spec/helpers/blame_helper_spec.rb
+++ b/spec/helpers/blame_helper_spec.rb
@@ -35,25 +35,32 @@ describe BlameHelper do
end
describe '#age_map_class' do
- let(:dates) do
- [Time.zone.local(2014, 3, 17, 0, 0, 0)]
- end
- let(:blame_groups) do
- [
- { commit: double(committed_date: dates[0]) }
- ]
- end
+ let(:date) { Time.zone.local(2014, 3, 17, 0, 0, 0) }
+ let(:blame_groups) { [{ commit: double(committed_date: date) }] }
let(:duration) do
- project = double(created_at: dates[0])
+ project = double(created_at: date)
helper.age_map_duration(blame_groups, project)
end
it 'returns blame-commit-age-9 when oldest' do
- expect(helper.age_map_class(dates[0], duration)).to eq 'blame-commit-age-9'
+ expect(helper.age_map_class(date, duration)).to eq 'blame-commit-age-9'
end
it 'returns blame-commit-age-0 class when newest' do
expect(helper.age_map_class(duration[:now], duration)).to eq 'blame-commit-age-0'
end
+
+ context 'when called on the same day as project creation' do
+ let(:same_day_duration) do
+ project = double(created_at: now)
+ helper.age_map_duration(today_blame_groups, project)
+ end
+ let(:today_blame_groups) { [{ commit: double(committed_date: now) }] }
+ let(:now) { Time.zone.now }
+
+ it 'returns blame-commit-age-0 class' do
+ expect(helper.age_map_class(duration[:now], same_day_duration)).to eq 'blame-commit-age-0'
+ end
+ end
end
end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index c654151564e..04620f6d88c 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -95,13 +95,13 @@ describe BlobHelper do
it 'returns a link with the proper route' do
link = edit_blob_link(project, 'master', 'README.md')
- expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md')
+ expect(Capybara.string(link).find_link('Edit')[:href]).to eq("/#{project.full_path}/edit/master/README.md")
end
it 'returns a link with the passed link_opts on the expected route' do
link = edit_blob_link(project, 'master', 'README.md', link_opts: { mr_id: 10 })
- expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md?mr_id=10')
+ expect(Capybara.string(link).find_link('Edit')[:href]).to eq("/#{project.full_path}/edit/master/README.md?mr_id=10")
end
end
diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb
index 250ba239033..4423560ecaa 100644
--- a/spec/helpers/button_helper_spec.rb
+++ b/spec/helpers/button_helper_spec.rb
@@ -62,4 +62,67 @@ describe ButtonHelper do
end
end
end
+
+ describe 'clipboard_button' do
+ let(:user) { create(:user) }
+ let(:project) { build_stubbed(:project) }
+
+ def element(data = {})
+ element = helper.clipboard_button(data)
+ Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
+ end
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ context 'with default options' do
+ context 'when no `text` attribute is not provided' do
+ it 'shows copy to clipboard button with default configuration and no text set to copy' do
+ expect(element.attr('class')).to eq('btn btn-clipboard btn-transparent')
+ expect(element.attr('type')).to eq('button')
+ expect(element.attr('aria-label')).to eq('Copy to clipboard')
+ expect(element.attr('data-toggle')).to eq('tooltip')
+ expect(element.attr('data-placement')).to eq('bottom')
+ expect(element.attr('data-container')).to eq('body')
+ expect(element.attr('data-clipboard-text')).to eq(nil)
+ expect(element.inner_text).to eq("")
+
+ expect(element).to have_selector('.fa.fa-clipboard')
+ end
+ end
+
+ context 'when `text` attribute is provided' do
+ it 'shows copy to clipboard button with provided `text` to copy' do
+ expect(element(text: 'Hello World!').attr('data-clipboard-text')).to eq('Hello World!')
+ end
+ end
+
+ context 'when `title` attribute is provided' do
+ it 'shows copy to clipboard button with provided `title` as tooltip' do
+ expect(element(title: 'Copy to my clipboard!').attr('aria-label')).to eq('Copy to my clipboard!')
+ end
+ end
+ end
+
+ context 'with `button_text` attribute provided' do
+ it 'shows copy to clipboard button with provided `button_text` as button label' do
+ expect(element(button_text: 'Copy text').inner_text).to eq('Copy text')
+ end
+ end
+
+ context 'with `hide_tooltip` attribute provided' do
+ it 'shows copy to clipboard button without tooltip support' do
+ expect(element(hide_tooltip: true).attr('data-placement')).to eq(nil)
+ expect(element(hide_tooltip: true).attr('data-toggle')).to eq(nil)
+ expect(element(hide_tooltip: true).attr('data-container')).to eq(nil)
+ end
+ end
+
+ context 'with `hide_button_icon` attribute provided' do
+ it 'shows copy to clipboard button without tooltip support' do
+ expect(element(hide_button_icon: true)).not_to have_selector('.fa.fa-clipboard')
+ end
+ end
+ end
end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index f81a9b6492c..0deea0ff6a3 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -135,10 +135,10 @@ describe DiffHelper do
it "returns strings with marked inline diffs" 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">&#39;def&#39;</span>})
- expect(marked_old_line).to be_html_safe
- expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">&quot;def&quot;</span>})
- expect(marked_new_line).to be_html_safe
+ 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_new_line).to eq(%q{abc <span class="idiff left right addition">"def"</span>})
+ expect(marked_new_line).not_to be_html_safe
end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index aa138f25bd3..d5536fcb22b 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -62,6 +62,12 @@ describe EventsHelper do
expect(helper.event_note(input)).to eq(expected)
end
+ it 'preserves data-src for lazy images' do
+ input = "![ImageTest](/uploads/test.png)"
+ image_url = "data-src=\"/uploads/test.png\""
+ expect(helper.event_note(input)).to match(image_url)
+ end
+
context 'labels formatting' do
let(:input) { 'this should be ~label_1' }
@@ -100,5 +106,9 @@ describe EventsHelper do
it "handles empty strings" do
expect(helper.event_commit_title("")).to eq("")
end
+
+ it 'handles nil values' do
+ expect(helper.event_commit_title(nil)).to eq('')
+ end
end
end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 9d6e03e3868..36031ac1a28 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -91,7 +91,101 @@ describe GroupsHelper do
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
it 'outputs the groups in the correct order' do
- expect(helper.group_title(very_deep_nested_group)).to match(/>#{group.name}<\/a>.*>#{nested_group.name}<\/a>.*>#{deep_nested_group.name}<\/a>/)
+ expect(helper.group_title(very_deep_nested_group))
+ .to match(/<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*<\/li>.*<a.*>#{very_deep_nested_group.name}<\/a>/m)
+ end
+ end
+
+ # rubocop:disable Layout/SpaceBeforeComma
+ describe '#share_with_group_lock_help_text', :nested_groups do
+ let!(:root_group) { create(:group) }
+ let!(:subgroup) { create(:group, parent: root_group) }
+ let!(:sub_subgroup) { create(:group, parent: subgroup) }
+ let(:root_owner) { create(:user) }
+ let(:sub_owner) { create(:user) }
+ let(:sub_sub_owner) { create(:user) }
+ let(:possible_help_texts) do
+ {
+ default_help: "This setting will be applied to all subgroups unless overridden by a group owner",
+ ancestor_locked_but_you_can_override: /This setting is applied on <a .+>.+<\/a>\. You can override the setting or .+/,
+ ancestor_locked_so_ask_the_owner: /This setting is applied on .+\. To share projects in this group with another group, ask the owner to override the setting or remove the share with group lock from .+/,
+ ancestor_locked_and_has_been_overridden: /This setting is applied on .+ and has been overridden on this subgroup/
+ }
+ end
+ let(:possible_linked_ancestors) do
+ {
+ root_group: root_group,
+ subgroup: subgroup
+ }
+ end
+ let(:users) do
+ {
+ root_owner: root_owner,
+ sub_owner: sub_owner,
+ sub_sub_owner: sub_sub_owner
+ }
+ end
+ subject { helper.share_with_group_lock_help_text(sub_subgroup) }
+
+ where(:root_share_with_group_locked, :subgroup_share_with_group_locked, :sub_subgroup_share_with_group_locked, :current_user, :help_text, :linked_ancestor) do
+ [
+ [false , false , false , :root_owner , :default_help , nil],
+ [false , false , false , :sub_owner , :default_help , nil],
+ [false , false , false , :sub_sub_owner , :default_help , nil],
+ [false , false , true , :root_owner , :default_help , nil],
+ [false , false , true , :sub_owner , :default_help , nil],
+ [false , false , true , :sub_sub_owner , :default_help , nil],
+ [false , true , false , :root_owner , :ancestor_locked_and_has_been_overridden , :subgroup],
+ [false , true , false , :sub_owner , :ancestor_locked_and_has_been_overridden , :subgroup],
+ [false , true , false , :sub_sub_owner , :ancestor_locked_and_has_been_overridden , :subgroup],
+ [false , true , true , :root_owner , :ancestor_locked_but_you_can_override , :subgroup],
+ [false , true , true , :sub_owner , :ancestor_locked_but_you_can_override , :subgroup],
+ [false , true , true , :sub_sub_owner , :ancestor_locked_so_ask_the_owner , :subgroup],
+ [true , false , false , :root_owner , :default_help , nil],
+ [true , false , false , :sub_owner , :default_help , nil],
+ [true , false , false , :sub_sub_owner , :default_help , nil],
+ [true , false , true , :root_owner , :default_help , nil],
+ [true , false , true , :sub_owner , :default_help , nil],
+ [true , false , true , :sub_sub_owner , :default_help , nil],
+ [true , true , false , :root_owner , :ancestor_locked_and_has_been_overridden , :root_group],
+ [true , true , false , :sub_owner , :ancestor_locked_and_has_been_overridden , :root_group],
+ [true , true , false , :sub_sub_owner , :ancestor_locked_and_has_been_overridden , :root_group],
+ [true , true , true , :root_owner , :ancestor_locked_but_you_can_override , :root_group],
+ [true , true , true , :sub_owner , :ancestor_locked_so_ask_the_owner , :root_group],
+ [true , true , true , :sub_sub_owner , :ancestor_locked_so_ask_the_owner , :root_group]
+ ]
+ end
+
+ with_them do
+ before do
+ root_group.add_owner(root_owner)
+ subgroup.add_owner(sub_owner)
+ sub_subgroup.add_owner(sub_sub_owner)
+
+ root_group.update_column(:share_with_group_lock, true) if root_share_with_group_locked
+ subgroup.update_column(:share_with_group_lock, true) if subgroup_share_with_group_locked
+ sub_subgroup.update_column(:share_with_group_lock, true) if sub_subgroup_share_with_group_locked
+
+ allow(helper).to receive(:current_user).and_return(users[current_user])
+ allow(helper).to receive(:can?)
+ .with(users[current_user], :change_share_with_group_lock, subgroup)
+ .and_return(Ability.allowed?(users[current_user], :change_share_with_group_lock, subgroup))
+
+ ancestor = possible_linked_ancestors[linked_ancestor]
+ if ancestor
+ allow(helper).to receive(:can?)
+ .with(users[current_user], :read_group, ancestor)
+ .and_return(Ability.allowed?(users[current_user], :read_group, ancestor))
+ allow(helper).to receive(:can?)
+ .with(users[current_user], :admin_group, ancestor)
+ .and_return(Ability.allowed?(users[current_user], :admin_group, ancestor))
+ end
+ end
+
+ it 'has the correct help text with correct ancestor links' do
+ expect(subject).to match(possible_help_texts[help_text])
+ expect(subject).to match(possible_linked_ancestors[linked_ancestor].name) unless help_text == :default_help
+ end
end
end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 7789cfa3554..ead3e28438e 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -59,112 +59,6 @@ describe IssuablesHelper do
.to eq('<span>All</span> <span class="badge">42</span>')
end
end
-
- describe 'counter caching based on issuable type and params', :use_clean_rails_memory_store_caching do
- let(:params) do
- {
- scope: 'created-by-me',
- state: 'opened',
- utf8: '✓',
- author_id: '11',
- assignee_id: '18',
- label_name: %w(bug discussion documentation),
- milestone_title: 'v4.0',
- sort: 'due_date_asc',
- namespace_id: 'gitlab-org',
- project_id: 'gitlab-ce',
- page: 2
- }.with_indifferent_access
- end
-
- let(:issues_finder) { IssuesFinder.new(nil, params) }
- let(:merge_requests_finder) { MergeRequestsFinder.new(nil, params) }
-
- before do
- allow(helper).to receive(:issues_finder).and_return(issues_finder)
- allow(helper).to receive(:merge_requests_finder).and_return(merge_requests_finder)
- end
-
- it 'returns the cached value when called for the same issuable type & with the same params' do
- expect(issues_finder).to receive(:count_by_state).and_return(opened: 42)
-
- expect(helper.issuables_state_counter_text(:issues, :opened))
- .to eq('<span>Open</span> <span class="badge">42</span>')
-
- expect(issues_finder).not_to receive(:count_by_state)
-
- expect(helper.issuables_state_counter_text(:issues, :opened))
- .to eq('<span>Open</span> <span class="badge">42</span>')
- end
-
- it 'takes confidential status into account when searching for issues' do
- expect(issues_finder).to receive(:count_by_state).and_return(opened: 42)
-
- expect(helper.issuables_state_counter_text(:issues, :opened))
- .to include('42')
-
- expect(issues_finder).to receive(:user_cannot_see_confidential_issues?).twice.and_return(false)
- expect(issues_finder).to receive(:count_by_state).and_return(opened: 40)
-
- expect(helper.issuables_state_counter_text(:issues, :opened))
- .to include('40')
-
- expect(issues_finder).to receive(:user_can_see_all_confidential_issues?).and_return(true)
- expect(issues_finder).to receive(:count_by_state).and_return(opened: 45)
-
- expect(helper.issuables_state_counter_text(:issues, :opened))
- .to include('45')
- end
-
- it 'does not take confidential status into account when searching for merge requests' do
- expect(merge_requests_finder).to receive(:count_by_state).and_return(opened: 42)
- expect(merge_requests_finder).not_to receive(:user_cannot_see_confidential_issues?)
- expect(merge_requests_finder).not_to receive(:user_can_see_all_confidential_issues?)
-
- expect(helper.issuables_state_counter_text(:merge_requests, :opened))
- .to include('42')
- end
-
- it 'does not take some keys into account in the cache key' do
- expect(issues_finder).to receive(:count_by_state).and_return(opened: 42)
- expect(issues_finder).to receive(:params).and_return({
- author_id: '11',
- state: 'foo',
- sort: 'foo',
- utf8: 'foo',
- page: 'foo'
- }.with_indifferent_access)
-
- expect(helper.issuables_state_counter_text(:issues, :opened))
- .to eq('<span>Open</span> <span class="badge">42</span>')
-
- expect(issues_finder).not_to receive(:count_by_state)
- expect(issues_finder).to receive(:params).and_return({
- author_id: '11',
- state: 'bar',
- sort: 'bar',
- utf8: 'bar',
- page: 'bar'
- }.with_indifferent_access)
-
- expect(helper.issuables_state_counter_text(:issues, :opened))
- .to eq('<span>Open</span> <span class="badge">42</span>')
- end
-
- it 'does not take params order into account in the cache key' do
- expect(issues_finder).to receive(:params).and_return('author_id' => '11', 'state' => 'opened')
- expect(issues_finder).to receive(:count_by_state).and_return(opened: 42)
-
- expect(helper.issuables_state_counter_text(:issues, :opened))
- .to eq('<span>Open</span> <span class="badge">42</span>')
-
- expect(issues_finder).to receive(:params).and_return('state' => 'opened', 'author_id' => '11')
- expect(issues_finder).not_to receive(:count_by_state)
-
- expect(helper.issuables_state_counter_text(:issues, :opened))
- .to eq('<span>Open</span> <span class="badge">42</span>')
- end
- end
end
describe '#issuable_reference' do
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index dc3100311f8..ddf881a7b6f 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -58,16 +58,6 @@ describe IssuesHelper do
end
end
- describe "merge_requests_sentence" do
- subject { merge_requests_sentence(merge_requests)}
- let(:merge_requests) do
- [build(:merge_request, iid: 1), build(:merge_request, iid: 2),
- build(:merge_request, iid: 3)]
- end
-
- it { is_expected.to eq("!1, !2, or !3") }
- end
-
describe '#award_user_list' do
it "returns a comma-separated list of the first X users" do
user = build_stubbed(:user, name: 'Joe')
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 70eb01c9c44..03d706062b7 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -52,12 +52,71 @@ describe MarkupHelper do
end
end
- describe '#link_to_gfm' do
+ describe '#markdown_field' do
+ let(:attribute) { :title }
+
+ describe 'with already redacted attribute' do
+ it 'returns the redacted attribute' do
+ commit.redacted_title_html = 'commit title'
+
+ expect(Banzai).not_to receive(:render_field)
+
+ expect(helper.markdown_field(commit, attribute)).to eq('commit title')
+ end
+ end
+
+ describe 'without redacted attribute' do
+ it 'renders the markdown value' do
+ expect(Banzai).to receive(:render_field).with(commit, attribute).and_call_original
+
+ helper.markdown_field(commit, attribute)
+ end
+ end
+ end
+
+ describe '#link_to_markdown_field' do
+ let(:link) { '/commits/0a1b2c3d' }
+ let(:issues) { create_list(:issue, 2, project: project) }
+
+ it 'handles references nested in links with all the text' do
+ allow(commit).to receive(:title).and_return("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real")
+
+ actual = helper.link_to_markdown_field(commit, :title, link)
+ doc = Nokogiri::HTML.parse(actual)
+
+ # Make sure we didn't create invalid markup
+ expect(doc.errors).to be_empty
+
+ # Leading commit link
+ expect(doc.css('a')[0].attr('href')).to eq link
+ expect(doc.css('a')[0].text).to eq 'This should finally fix '
+
+ # First issue link
+ expect(doc.css('a')[1].attr('href'))
+ .to eq project_issue_path(project, issues[0])
+ expect(doc.css('a')[1].text).to eq issues[0].to_reference
+
+ # Internal commit link
+ expect(doc.css('a')[2].attr('href')).to eq link
+ expect(doc.css('a')[2].text).to eq ' and '
+
+ # Second issue link
+ expect(doc.css('a')[3].attr('href'))
+ .to eq project_issue_path(project, issues[1])
+ expect(doc.css('a')[3].text).to eq issues[1].to_reference
+
+ # Trailing commit link
+ expect(doc.css('a')[4].attr('href')).to eq link
+ expect(doc.css('a')[4].text).to eq ' for real'
+ end
+ end
+
+ describe '#link_to_markdown' do
let(:link) { '/commits/0a1b2c3d' }
let(:issues) { create_list(:issue, 2, project: project) }
it 'handles references nested in links with all the text' do
- actual = helper.link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", link)
+ actual = helper.link_to_markdown("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", link)
doc = Nokogiri::HTML.parse(actual)
# Make sure we didn't create invalid markup
@@ -87,7 +146,7 @@ describe MarkupHelper do
end
it 'forwards HTML options' do
- actual = helper.link_to_gfm("Fixed in #{commit.id}", link, class: 'foo')
+ actual = helper.link_to_markdown("Fixed in #{commit.id}", link, class: 'foo')
doc = Nokogiri::HTML.parse(actual)
expect(doc.css('a')).to satisfy do |v|
@@ -98,23 +157,43 @@ describe MarkupHelper do
it "escapes HTML passed in as the body" do
actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}"
- expect(helper.link_to_gfm(actual, link))
+ expect(helper.link_to_markdown(actual, link))
.to match('&lt;h1&gt;test&lt;/h1&gt;')
end
it 'ignores reference links when they are the entire body' do
text = issues[0].to_reference
- act = helper.link_to_gfm(text, '/foo')
+ act = helper.link_to_markdown(text, '/foo')
expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>)
end
it 'replaces commit message with emoji to link' do
- actual = link_to_gfm(':book: Book', '/foo')
+ actual = link_to_markdown(':book: Book', '/foo')
expect(actual)
.to eq '<gl-emoji title="open book" data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo"> Book</a>'
end
end
+ describe '#link_to_html' do
+ it 'wraps the rendered content in a link' do
+ link = '/commits/0a1b2c3d'
+ issue = create(:issue, project: project)
+
+ rendered = helper.markdown("This should finally fix #{issue.to_reference} for real", pipeline: :single_line)
+ doc = Nokogiri::HTML.parse(rendered)
+
+ expect(doc.css('a')[0].attr('href'))
+ .to eq project_issue_path(project, issue)
+ expect(doc.css('a')[0].text).to eq issue.to_reference
+
+ wrapped = helper.link_to_html(rendered, link)
+ doc = Nokogiri::HTML.parse(wrapped)
+
+ expect(doc.css('a')[0].attr('href')).to eq link
+ expect(doc.css('a')[0].text).to eq 'This should finally fix '
+ end
+ end
+
describe '#render_wiki_content' do
before do
@wiki = double('WikiPage')
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index 9921ca1af33..cd15e27b497 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -23,10 +23,10 @@ describe NotesHelper do
end
describe "#notes_max_access_for_users" do
- it 'returns human access levels' do
- expect(helper.note_max_access_for_user(owner_note)).to eq('Owner')
- expect(helper.note_max_access_for_user(master_note)).to eq('Master')
- expect(helper.note_max_access_for_user(reporter_note)).to eq('Reporter')
+ it 'returns access levels' do
+ expect(helper.note_max_access_for_user(owner_note)).to eq(Gitlab::Access::OWNER)
+ expect(helper.note_max_access_for_user(master_note)).to eq(Gitlab::Access::MASTER)
+ expect(helper.note_max_access_for_user(reporter_note)).to eq(Gitlab::Access::REPORTER)
end
it 'handles access in different projects' do
@@ -34,8 +34,8 @@ describe NotesHelper do
second_project.team << [master, :reporter]
other_note = create(:note, author: master, project: second_project)
- expect(helper.note_max_access_for_user(master_note)).to eq('Master')
- expect(helper.note_max_access_for_user(other_note)).to eq('Reporter')
+ expect(helper.note_max_access_for_user(master_note)).to eq(Gitlab::Access::MASTER)
+ expect(helper.note_max_access_for_user(other_note)).to eq(Gitlab::Access::REPORTER)
end
end
@@ -231,7 +231,7 @@ describe NotesHelper do
end
end
- describe '#form_resurces' do
+ describe '#form_resources' do
it 'returns note for personal snippet' do
@snippet = create(:personal_snippet)
@note = create(:note_on_personal_snippet)
@@ -266,4 +266,22 @@ describe NotesHelper do
expect(noteable_note_url(note)).to match("/#{project.namespace.path}/#{project.path}/issues/#{issue.iid}##{dom_id(note)}")
end
end
+
+ describe '#discussion_resolved_intro' do
+ context 'when the discussion was resolved by a push' do
+ let(:discussion) { double(:discussion, resolved_by_push?: true) }
+
+ it 'returns "Automatically resolved"' do
+ expect(discussion_resolved_intro(discussion)).to eq('Automatically resolved')
+ end
+ end
+
+ context 'when the discussion was not resolved by a push' do
+ let(:discussion) { double(:discussion, resolved_by_push?: false) }
+
+ it 'returns "Resolved"' do
+ expect(discussion_resolved_intro(discussion)).to eq('Resolved')
+ end
+ end
+ end
end
diff --git a/spec/helpers/pagination_helper_spec.rb b/spec/helpers/pagination_helper_spec.rb
new file mode 100644
index 00000000000..e235475fb47
--- /dev/null
+++ b/spec/helpers/pagination_helper_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe PaginationHelper do
+ describe '#paginate_collection' do
+ let(:collection) { User.all.page(1) }
+
+ it 'paginates a collection without using a COUNT' do
+ without_count = collection.without_count
+
+ expect(helper).to receive(:paginate_without_count)
+ .with(without_count)
+ .and_call_original
+
+ helper.paginate_collection(without_count)
+ end
+
+ it 'paginates a collection using a COUNT' do
+ expect(helper).to receive(:paginate_with_count).and_call_original
+
+ helper.paginate_collection(collection)
+ end
+ end
+end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index a04c87b08eb..8b8080563d3 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe PreferencesHelper do
- describe 'dashboard_choices' do
+ describe '#dashboard_choices' do
it 'raises an exception when defined choices may be missing' do
expect(User).to receive(:dashboards).and_return(foo: 'foo')
expect { helper.dashboard_choices }.to raise_error(RuntimeError)
@@ -26,7 +26,33 @@ describe PreferencesHelper do
end
end
- describe 'user_color_scheme' do
+ describe '#user_application_theme' do
+ context 'with a user' do
+ it "returns user's theme's css_class" do
+ stub_user(theme_id: 3)
+
+ expect(helper.user_application_theme).to eq 'ui_light'
+ end
+
+ it 'returns the default when id is invalid' do
+ stub_user(theme_id: Gitlab::Themes.count + 5)
+
+ allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(1)
+
+ expect(helper.user_application_theme).to eq 'ui_indigo'
+ end
+ end
+
+ context 'without a user' do
+ it 'returns the default theme' do
+ stub_user
+
+ expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class
+ end
+ end
+ end
+
+ describe '#user_color_scheme' do
context 'with a user' do
it "returns user's scheme's css_class" do
allow(helper).to receive(:current_user)
diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb
index b33b3f3a228..c1d0614c79e 100644
--- a/spec/helpers/profiles_helper_spec.rb
+++ b/spec/helpers/profiles_helper_spec.rb
@@ -6,22 +6,41 @@ describe ProfilesHelper do
user = create(:user)
allow(helper).to receive(:current_user).and_return(user)
- expect(helper.email_provider_label).to be_nil
+ expect(helper.attribute_provider_label(:email)).to be_nil
end
- it "returns omniauth provider label for users with external email" do
+ it "returns omniauth provider label for users with external attributes" do
+ stub_omniauth_setting(sync_profile_from_provider: ['cas3'])
+ stub_omniauth_setting(sync_profile_attributes: true)
stub_cas_omniauth_provider
- cas_user = create(:omniauth_user, provider: 'cas3', external_email: true, email_provider: 'cas3')
+ cas_user = create(:omniauth_user, provider: 'cas3')
+ cas_user.create_user_synced_attributes_metadata(provider: 'cas3', name_synced: true, email_synced: true, location_synced: true)
allow(helper).to receive(:current_user).and_return(cas_user)
- expect(helper.email_provider_label).to eq('CAS')
+ expect(helper.attribute_provider_label(:email)).to eq('CAS')
+ expect(helper.attribute_provider_label(:name)).to eq('CAS')
+ expect(helper.attribute_provider_label(:location)).to eq('CAS')
+ end
+
+ it "returns the correct omniauth provider label for users with some external attributes" do
+ stub_omniauth_setting(sync_profile_from_provider: ['cas3'])
+ stub_omniauth_setting(sync_profile_attributes: true)
+ stub_cas_omniauth_provider
+ cas_user = create(:omniauth_user, provider: 'cas3')
+ cas_user.create_user_synced_attributes_metadata(provider: 'cas3', name_synced: false, email_synced: true, location_synced: false)
+ allow(helper).to receive(:current_user).and_return(cas_user)
+
+ expect(helper.attribute_provider_label(:name)).to be_nil
+ expect(helper.attribute_provider_label(:email)).to eq('CAS')
+ expect(helper.attribute_provider_label(:location)).to be_nil
end
it "returns 'LDAP' for users with external email but no email provider" do
- ldap_user = create(:omniauth_user, external_email: true)
+ ldap_user = create(:omniauth_user)
+ ldap_user.create_user_synced_attributes_metadata(email_synced: true)
allow(helper).to receive(:current_user).and_return(ldap_user)
- expect(helper.email_provider_label).to eq('LDAP')
+ expect(helper.attribute_provider_label(:email)).to eq('LDAP')
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 37a5e6b474e..49cb7c954b4 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -191,10 +191,31 @@ describe ProjectsHelper do
end
end
- describe 'link_to_member' do
- let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
- let(:user) { create(:user) }
+ describe '#link_to_member_avatar' do
+ let(:user) { build_stubbed(:user) }
+ let(:expected) { double }
+
+ before do
+ expect(helper).to receive(:avatar_icon).with(user, 16).and_return(expected)
+ 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: "" })
+
+ 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: "" })
+
+ helper.link_to_member_avatar(user, avatar_class: "any-avatar-class")
+ end
+ end
+
+ describe '#link_to_member' do
+ let(:group) { build_stubbed(:group) }
+ let(:project) { build_stubbed(:project, group: group) }
+ let(:user) { build_stubbed(:user) }
describe 'using the default options' do
it 'returns an HTML link to the user' do
@@ -432,9 +453,7 @@ describe ProjectsHelper do
end
describe '#any_projects?' do
- before do
- create(:project)
- end
+ let!(:project) { create(:project) }
it 'returns true when projects will be returned' do
expect(helper.any_projects?(Project.all)).to eq(true)
@@ -444,6 +463,14 @@ describe ProjectsHelper do
expect(helper.any_projects?(Project.none)).to eq(false)
end
+ it 'returns true when using a non-empty Array' do
+ expect(helper.any_projects?([project])).to eq(true)
+ end
+
+ it 'returns false when using an empty Array' do
+ expect(helper.any_projects?([])).to eq(false)
+ end
+
it 'only executes a single query when a LIMIT is applied' do
relation = Project.limit(1)
recorder = ActiveRecord::QueryRecorder.new do
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 463af15930d..ab647401e14 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -17,7 +17,7 @@ describe SearchHelper do
end
end
- context "with a user" do
+ context "with a standard user" do
let(:user) { create(:user) }
before do
@@ -29,7 +29,11 @@ describe SearchHelper do
end
it "includes default sections" do
- expect(search_autocomplete_opts("adm").size).to eq(1)
+ expect(search_autocomplete_opts("dash").size).to eq(1)
+ end
+
+ it "does not include admin sections" do
+ expect(search_autocomplete_opts("admin").size).to eq(0)
end
it "does not allow regular expression in search term" do
@@ -67,6 +71,18 @@ describe SearchHelper do
end
end
end
+
+ context 'with an admin user' do
+ let(:admin) { create(:admin) }
+
+ before do
+ allow(self).to receive(:current_user).and_return(admin)
+ end
+
+ it "includes admin sections" do
+ expect(search_autocomplete_opts("admin").size).to eq(1)
+ end
+ end
end
describe 'search_filter_input_options' do
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index 9523d0f4aa6..d7b66e6f078 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -3,25 +3,35 @@ require 'spec_helper'
describe TreeHelper do
describe 'flatten_tree' do
let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:sha) { 'ce369011c189f62c815f5971d096b26759bab0d1' }
+ let(:tree) { repository.tree(sha, 'files') }
+ let(:root_path) { 'files' }
+ let(:tree_item) { tree.entries.find { |entry| entry.path == path } }
- before do
- @repository = project.repository
- @commit = project.commit("e56497bb")
- end
+ subject { flatten_tree(root_path, tree_item) }
context "on a directory containing more than one file/directory" do
- let(:tree_item) { double(name: "files", path: "files") }
+ let(:path) { 'files/html' }
it "returns the directory name" do
- expect(flatten_tree(tree_item)).to match('files')
+ expect(subject).to match('html')
end
end
context "on a directory containing only one directory" do
- let(:tree_item) { double(name: "foo", path: "foo") }
+ let(:path) { 'files/flat' }
it "returns the flattened path" do
- expect(flatten_tree(tree_item)).to match('foo/bar')
+ expect(subject).to match('flat/path/correct')
+ end
+
+ context "with a nested root path" do
+ let(:root_path) { 'files/flat' }
+
+ it "returns the flattened path with the root path suffix removed" do
+ expect(subject).to match('path/correct')
+ end
end
end
end
diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
index 889fe441171..fa8cfda3b86 100644
--- a/spec/helpers/version_check_helper_spec.rb
+++ b/spec/helpers/version_check_helper_spec.rb
@@ -4,7 +4,7 @@ describe VersionCheckHelper do
describe '#version_status_badge' do
it 'should return nil if not dev environment and not enabled' do
allow(Rails.env).to receive(:production?) { false }
- allow(current_application_settings).to receive(:version_check_enabled) { false }
+ allow(helper.current_application_settings).to receive(:version_check_enabled) { false }
expect(helper.version_status_badge).to be(nil)
end
@@ -12,7 +12,7 @@ describe VersionCheckHelper do
context 'when production and enabled' do
before do
allow(Rails.env).to receive(:production?) { true }
- allow(current_application_settings).to receive(:version_check_enabled) { true }
+ allow(helper.current_application_settings).to receive(:version_check_enabled) { true }
allow_any_instance_of(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' }
@image_tag = helper.version_status_badge
@@ -23,7 +23,7 @@ describe VersionCheckHelper do
end
it 'should have a js prefixed css class' do
- expect(@image_tag).to match(/class="js-version-status-badge"/)
+ expect(@image_tag).to match(/class="js-version-status-badge lazy"/)
end
it 'should have a VersionCheck url as the src' do
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index c3cccbb0d95..5077c89d7b4 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -58,35 +58,82 @@ describe VisibilityLevelHelper do
end
end
- describe "skip_level?" do
+ describe "disallowed_visibility_level?" do
describe "forks" do
let(:project) { create(:project, :internal) }
let(:fork_project) { create(:project, forked_from_project: project) }
- it "skips levels" do
- expect(skip_level?(fork_project, Gitlab::VisibilityLevel::PUBLIC)).to be_truthy
- expect(skip_level?(fork_project, Gitlab::VisibilityLevel::INTERNAL)).to be_falsey
- expect(skip_level?(fork_project, Gitlab::VisibilityLevel::PRIVATE)).to be_falsey
+ it "disallows levels" do
+ expect(disallowed_visibility_level?(fork_project, Gitlab::VisibilityLevel::PUBLIC)).to be_truthy
+ expect(disallowed_visibility_level?(fork_project, Gitlab::VisibilityLevel::INTERNAL)).to be_falsey
+ expect(disallowed_visibility_level?(fork_project, Gitlab::VisibilityLevel::PRIVATE)).to be_falsey
end
end
describe "non-forked project" do
let(:project) { create(:project, :internal) }
- it "skips levels" do
- expect(skip_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey
- expect(skip_level?(project, Gitlab::VisibilityLevel::INTERNAL)).to be_falsey
- expect(skip_level?(project, Gitlab::VisibilityLevel::PRIVATE)).to be_falsey
+ it "disallows levels" do
+ expect(disallowed_visibility_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey
+ expect(disallowed_visibility_level?(project, Gitlab::VisibilityLevel::INTERNAL)).to be_falsey
+ expect(disallowed_visibility_level?(project, Gitlab::VisibilityLevel::PRIVATE)).to be_falsey
end
end
- describe "Snippet" do
+ describe "group" do
+ let(:group) { create(:group, :internal) }
+
+ it "disallows levels" do
+ expect(disallowed_visibility_level?(group, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey
+ expect(disallowed_visibility_level?(group, Gitlab::VisibilityLevel::INTERNAL)).to be_falsey
+ expect(disallowed_visibility_level?(group, Gitlab::VisibilityLevel::PRIVATE)).to be_falsey
+ end
+ end
+
+ describe "sub-group" do
+ let(:group) { create(:group, :private) }
+ let(:subgroup) { create(:group, :private, parent: group) }
+
+ it "disallows levels" do
+ expect(disallowed_visibility_level?(subgroup, Gitlab::VisibilityLevel::PUBLIC)).to be_truthy
+ expect(disallowed_visibility_level?(subgroup, Gitlab::VisibilityLevel::INTERNAL)).to be_truthy
+ expect(disallowed_visibility_level?(subgroup, Gitlab::VisibilityLevel::PRIVATE)).to be_falsey
+ end
+ end
+
+ describe "snippet" do
let(:snippet) { create(:snippet, :internal) }
- it "skips levels" do
- expect(skip_level?(snippet, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey
- expect(skip_level?(snippet, Gitlab::VisibilityLevel::INTERNAL)).to be_falsey
- expect(skip_level?(snippet, Gitlab::VisibilityLevel::PRIVATE)).to be_falsey
+ it "disallows levels" do
+ expect(disallowed_visibility_level?(snippet, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey
+ expect(disallowed_visibility_level?(snippet, Gitlab::VisibilityLevel::INTERNAL)).to be_falsey
+ expect(disallowed_visibility_level?(snippet, Gitlab::VisibilityLevel::PRIVATE)).to be_falsey
+ end
+ end
+ end
+
+ describe "disallowed_visibility_level_description" do
+ let(:group) { create(:group, :internal) }
+ let!(:subgroup) { create(:group, :internal, parent: group) }
+ let!(:project) { create(:project, :internal, group: group) }
+
+ describe "project" do
+ it "provides correct description for disabled levels" do
+ expect(disallowed_visibility_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_truthy
+ expect(strip_tags disallowed_visibility_level_description(Gitlab::VisibilityLevel::PUBLIC, project))
+ .to include "the visibility of #{project.group.name} is internal"
+ end
+ end
+
+ describe "group" do
+ it "provides correct description for disabled levels" do
+ expect(disallowed_visibility_level?(group, Gitlab::VisibilityLevel::PRIVATE)).to be_truthy
+ expect(disallowed_visibility_level_description(Gitlab::VisibilityLevel::PRIVATE, group))
+ .to include "it contains projects with higher visibility", "it contains sub-groups with higher visibility"
+
+ expect(disallowed_visibility_level?(subgroup, Gitlab::VisibilityLevel::PUBLIC)).to be_truthy
+ expect(strip_tags disallowed_visibility_level_description(Gitlab::VisibilityLevel::PUBLIC, subgroup))
+ .to include "the visibility of #{group.name} is internal"
end
end
end
diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb
index e5ec90cb8f9..9a974e70e8c 100644
--- a/spec/initializers/settings_spec.rb
+++ b/spec/initializers/settings_spec.rb
@@ -2,6 +2,22 @@ require 'spec_helper'
require_relative '../../config/initializers/1_settings'
describe Settings do
+ describe '#ldap' do
+ it 'can be accessed with dot syntax all the way down' do
+ expect(Gitlab.config.ldap.servers.main.label).to eq('ldap')
+ end
+
+ # Specifically trying to cause this error discovered in EE when removing the
+ # reassignment of each server element with Settingslogic.
+ #
+ # `undefined method `label' for #<Hash:0x007fbd18b59c08>`
+ #
+ it 'can be accessed in a very specific way that breaks without reassigning each element with Settingslogic' do
+ server_settings = Gitlab.config.ldap.servers['main']
+ expect(server_settings.label).to eq('ldap')
+ end
+ end
+
describe '#repositories' do
it 'assigns the default failure attributes' do
repository_settings = Gitlab.config.repositories.storages['broken']
@@ -11,6 +27,15 @@ describe Settings do
expect(repository_settings['failure_reset_time']).to eq(1800)
expect(repository_settings['storage_timeout']).to eq(5)
end
+
+ it 'can be accessed with dot syntax all the way down' do
+ expect(Gitlab.config.repositories.storages.broken.failure_count_threshold).to eq(10)
+ end
+
+ it 'can be accessed in a very specific way that breaks without reassigning each element with Settingslogic' do
+ storage_settings = Gitlab.config.repositories.storages['broken']
+ expect(storage_settings.failure_count_threshold).to eq(10)
+ end
end
describe '#host_without_www' do
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 867322ce8ae..2aa4fb1f6c6 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -17,7 +17,7 @@ describe('Api', () => {
beforeEach(() => {
originalGon = window.gon;
- window.gon = dummyGon;
+ window.gon = Object.assign({}, dummyGon);
});
afterEach(() => {
@@ -98,14 +98,38 @@ describe('Api', () => {
});
describe('projects', () => {
- it('fetches projects', (done) => {
+ it('fetches projects with membership when logged in', (done) => {
const query = 'dummy query';
const options = { unused: 'option' };
- const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`;
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
+ window.gon.current_user_id = 1;
const expectedData = Object.assign({
search: query,
per_page: 20,
membership: true,
+ simple: true,
+ }, options);
+ spyOn(jQuery, 'ajax').and.callFake((request) => {
+ expect(request.url).toEqual(expectedUrl);
+ expect(request.dataType).toEqual('json');
+ expect(request.data).toEqual(expectedData);
+ return sendDummyResponse();
+ });
+
+ Api.projects(query, options, (response) => {
+ expect(response).toBe(dummyResponse);
+ done();
+ });
+ });
+
+ it('fetches projects without membership when not logged in', (done) => {
+ const query = 'dummy query';
+ const options = { unused: 'option' };
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
+ const expectedData = Object.assign({
+ search: query,
+ per_page: 20,
+ simple: true,
}, options);
spyOn(jQuery, 'ajax').and.callFake((request) => {
expect(request.url).toEqual(expectedUrl);
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 8e056882108..a22b71fd1dc 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -25,9 +25,10 @@ import '~/lib/utils/common_utils';
};
describe('AwardsHandler', function() {
- preloadFixtures('issues/issue_with_comment.html.raw');
+ preloadFixtures('merge_requests/diff_comment.html.raw');
beforeEach(function(done) {
- loadFixtures('issues/issue_with_comment.html.raw');
+ loadFixtures('merge_requests/diff_comment.html.raw');
+ $('body').data('page', 'projects:merge_requests:show');
loadAwardsHandler(true).then((obj) => {
awardsHandler = obj;
spyOn(awardsHandler, 'postEmoji').and.callFake((button, url, emoji, cb) => cb());
@@ -139,7 +140,7 @@ import '~/lib/utils/common_utils';
});
describe('::getAwardUrl', function() {
return it('returns the url for request', function() {
- return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji');
+ return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1/toggle_award_emoji');
});
});
describe('::addAward and ::checkMutuality', function() {
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 6dc48f9a293..f62bf43adb9 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,119 +1,111 @@
-/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, max-len */
-
import '~/behaviors/quick_submit';
-(function() {
- describe('Quick Submit behavior', function() {
- var keydownEvent;
- preloadFixtures('issues/open-issue.html.raw');
- beforeEach(function() {
- loadFixtures('issues/open-issue.html.raw');
- $('form').submit(function(e) {
- // Prevent a form submit from moving us off the testing page
- return e.preventDefault();
- });
- this.spies = {
- submit: spyOnEvent('form', 'submit')
- };
+describe('Quick Submit behavior', () => {
+ const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options);
- this.textarea = $('.js-quick-submit textarea').first();
- });
- it('does not respond to other keyCodes', function() {
- this.textarea.trigger(keydownEvent({
- keyCode: 32
- }));
- return expect(this.spies.submit).not.toHaveBeenTriggered();
- });
- it('does not respond to Enter alone', function() {
- this.textarea.trigger(keydownEvent({
- ctrlKey: false,
- metaKey: false
- }));
- return expect(this.spies.submit).not.toHaveBeenTriggered();
- });
- it('does not respond to repeated events', function() {
- this.textarea.trigger(keydownEvent({
- repeat: true
- }));
- return expect(this.spies.submit).not.toHaveBeenTriggered();
- });
- it('disables input of type submit', function() {
- const submitButton = $('.js-quick-submit input[type=submit]');
- this.textarea.trigger(keydownEvent());
+ preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
- expect(submitButton).toBeDisabled();
+ beforeEach(() => {
+ loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+ $('body').attr('data-page', 'projects:merge_requests:show');
+ $('form').submit((e) => {
+ // Prevent a form submit from moving us off the testing page
+ e.preventDefault();
});
- it('disables button of type submit', function() {
- const submitButton = $('.js-quick-submit input[type=submit]');
- this.textarea.trigger(keydownEvent());
+ this.spies = {
+ submit: spyOnEvent('form', 'submit'),
+ };
- expect(submitButton).toBeDisabled();
- });
- it('only clicks one submit', function() {
- const existingSubmit = $('.js-quick-submit input[type=submit]');
- // Add an extra submit button
- const newSubmit = $('<button type="submit">Submit it</button>');
- newSubmit.insertAfter(this.textarea);
+ this.textarea = $('.js-quick-submit textarea').first();
+ });
- const oldClick = spyOnEvent(existingSubmit, 'click');
- const newClick = spyOnEvent(newSubmit, 'click');
+ it('does not respond to other keyCodes', () => {
+ this.textarea.trigger(keydownEvent({
+ keyCode: 32,
+ }));
+ expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
- this.textarea.trigger(keydownEvent());
+ it('does not respond to Enter alone', () => {
+ this.textarea.trigger(keydownEvent({
+ ctrlKey: false,
+ metaKey: false,
+ }));
+ expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
- expect(oldClick).not.toHaveBeenTriggered();
- expect(newClick).toHaveBeenTriggered();
- });
- // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll
- // only run the tests that apply to the current platform
- if (navigator.userAgent.match(/Macintosh/)) {
- it('responds to Meta+Enter', function() {
- this.textarea.trigger(keydownEvent());
- return expect(this.spies.submit).toHaveBeenTriggered();
- });
- it('excludes other modifier keys', function() {
- this.textarea.trigger(keydownEvent({
- altKey: true
- }));
- this.textarea.trigger(keydownEvent({
- ctrlKey: true
- }));
- this.textarea.trigger(keydownEvent({
- shiftKey: true
- }));
- return expect(this.spies.submit).not.toHaveBeenTriggered();
- });
- } else {
- it('responds to Ctrl+Enter', function() {
+ it('does not respond to repeated events', () => {
+ this.textarea.trigger(keydownEvent({
+ repeat: true,
+ }));
+ expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
+
+ it('disables input of type submit', () => {
+ const submitButton = $('.js-quick-submit input[type=submit]');
+ this.textarea.trigger(keydownEvent());
+
+ expect(submitButton).toBeDisabled();
+ });
+ it('disables button of type submit', () => {
+ const submitButton = $('.js-quick-submit input[type=submit]');
+ this.textarea.trigger(keydownEvent());
+
+ expect(submitButton).toBeDisabled();
+ });
+ it('only clicks one submit', () => {
+ const existingSubmit = $('.js-quick-submit input[type=submit]');
+ // Add an extra submit button
+ const newSubmit = $('<button type="submit">Submit it</button>');
+ newSubmit.insertAfter(this.textarea);
+
+ const oldClick = spyOnEvent(existingSubmit, 'click');
+ const newClick = spyOnEvent(newSubmit, 'click');
+
+ this.textarea.trigger(keydownEvent());
+
+ expect(oldClick).not.toHaveBeenTriggered();
+ expect(newClick).toHaveBeenTriggered();
+ });
+ // We cannot stub `navigator.userAgent` for CI's `rake karma` task, so we'll
+ // only run the tests that apply to the current platform
+ if (navigator.userAgent.match(/Macintosh/)) {
+ describe('In Macintosh', () => {
+ it('responds to Meta+Enter', () => {
this.textarea.trigger(keydownEvent());
return expect(this.spies.submit).toHaveBeenTriggered();
});
- it('excludes other modifier keys', function() {
+
+ it('excludes other modifier keys', () => {
this.textarea.trigger(keydownEvent({
- altKey: true
+ altKey: true,
}));
this.textarea.trigger(keydownEvent({
- metaKey: true
+ ctrlKey: true,
}));
this.textarea.trigger(keydownEvent({
- shiftKey: true
+ shiftKey: true,
}));
return expect(this.spies.submit).not.toHaveBeenTriggered();
});
- }
- return keydownEvent = function(options) {
- var defaults;
- if (navigator.userAgent.match(/Macintosh/)) {
- defaults = {
- keyCode: 13,
- metaKey: true
- };
- } else {
- defaults = {
- keyCode: 13,
- ctrlKey: true
- };
- }
- return $.Event('keydown', $.extend({}, defaults, options));
- };
- });
-}).call(window);
+ });
+ } else {
+ it('responds to Ctrl+Enter', () => {
+ this.textarea.trigger(keydownEvent());
+ return expect(this.spies.submit).toHaveBeenTriggered();
+ });
+
+ it('excludes other modifier keys', () => {
+ this.textarea.trigger(keydownEvent({
+ altKey: true,
+ }));
+ this.textarea.trigger(keydownEvent({
+ metaKey: true,
+ }));
+ this.textarea.trigger(keydownEvent({
+ shiftKey: true,
+ }));
+ return expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
+ }
+});
diff --git a/spec/javascripts/blob/blob_file_dropzone_spec.js b/spec/javascripts/blob/blob_file_dropzone_spec.js
new file mode 100644
index 00000000000..2c8183ff77b
--- /dev/null
+++ b/spec/javascripts/blob/blob_file_dropzone_spec.js
@@ -0,0 +1,42 @@
+import 'dropzone';
+import BlobFileDropzone from '~/blob/blob_file_dropzone';
+
+describe('BlobFileDropzone', () => {
+ preloadFixtures('blob/show.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('blob/show.html.raw');
+ const form = $('.js-upload-blob-form');
+ this.blobFileDropzone = new BlobFileDropzone(form, 'POST');
+ this.dropzone = $('.js-upload-blob-form .dropzone').get(0).dropzone;
+ this.replaceFileButton = $('#submit-all');
+ });
+
+ describe('submit button', () => {
+ it('requires file', () => {
+ spyOn(window, 'alert');
+
+ this.replaceFileButton.click();
+
+ expect(window.alert).toHaveBeenCalled();
+ });
+
+ it('is disabled while uploading', () => {
+ spyOn(window, 'alert');
+
+ const file = {
+ name: 'some-file.jpg',
+ type: 'jpg',
+ };
+ const fakeEvent = jQuery.Event('drop', {
+ dataTransfer: { files: [file] },
+ });
+
+ this.dropzone.listeners[0].events.drop(fakeEvent);
+ this.replaceFileButton.click();
+
+ expect(window.alert).not.toHaveBeenCalled();
+ expect(this.replaceFileButton.is(':disabled')).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js
index 47baf83512f..2ee3792dd65 100644
--- a/spec/javascripts/boards/board_blank_state_spec.js
+++ b/spec/javascripts/boards/board_blank_state_spec.js
@@ -1,4 +1,5 @@
/* global BoardService */
+/* global mockBoardService */
import Vue from 'vue';
import '~/boards/stores/boards_store';
import boardBlankState from '~/boards/components/board_blank_state';
@@ -12,7 +13,7 @@ describe('Boards blank state', () => {
const Comp = Vue.extend(boardBlankState);
gl.issueBoards.BoardsStore.create();
- gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.boardService = mockBoardService();
spyOn(gl.boardService, 'generateDefaultLists').and.callFake(() => new Promise((resolve, reject) => {
if (fail) {
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 447b244c71f..83b13b06dc1 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -4,6 +4,7 @@
/* global listObj */
/* global boardsMockInterceptor */
/* global BoardService */
+/* global mockBoardService */
import Vue from 'vue';
import '~/boards/models/assignee';
@@ -14,13 +15,13 @@ import '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card';
import './mock_data';
-describe('Issue card', () => {
+describe('Board card', () => {
let vm;
beforeEach((done) => {
Vue.http.interceptors.push(boardsMockInterceptor);
- gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
gl.issueBoards.BoardsStore.detail.issue = {};
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index a89be911667..6bd00943a8f 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -3,6 +3,7 @@
/* global List */
/* global listObj */
/* global ListIssue */
+/* global mockBoardService */
import Vue from 'vue';
import _ from 'underscore';
import Sortable from 'vendor/Sortable';
@@ -24,7 +25,7 @@ describe('Board list component', () => {
document.body.appendChild(el);
Vue.http.interceptors.push(boardsMockInterceptor);
- gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
gl.IssueBoardsApp = new Vue();
@@ -32,6 +33,7 @@ describe('Board list component', () => {
const list = new List(listObj);
const issue = new ListIssue({
title: 'Testing',
+ id: 1,
iid: 1,
confidential: false,
labels: [],
diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js
index c0a7323a505..02e6692dda8 100644
--- a/spec/javascripts/boards/board_new_issue_spec.js
+++ b/spec/javascripts/boards/board_new_issue_spec.js
@@ -2,6 +2,7 @@
/* global BoardService */
/* global List */
/* global listObj */
+/* global mockBoardService */
import Vue from 'vue';
import boardNewIssue from '~/boards/components/board_new_issue';
@@ -30,10 +31,12 @@ describe('Issue boards new issue form', () => {
};
beforeEach((done) => {
+ setFixtures('<div class="test-container"></div>');
+
const BoardNewIssueComp = Vue.extend(boardNewIssue);
Vue.http.interceptors.push(boardsMockInterceptor);
- gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
gl.IssueBoardsApp = new Vue();
@@ -46,15 +49,17 @@ describe('Issue boards new issue form', () => {
propsData: {
list,
},
- }).$mount();
+ }).$mount(document.querySelector('.test-container'));
Vue.nextTick()
.then(done)
.catch(done.fail);
});
+ afterEach(() => vm.$destroy());
+
it('calls submit if submit button is clicked', (done) => {
- spyOn(vm, 'submit');
+ spyOn(vm, 'submit').and.callFake(e => e.preventDefault());
vm.title = 'Testing Title';
Vue.nextTick()
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 5ea160b7790..9e5b0bd3efe 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -4,6 +4,7 @@
/* global listObj */
/* global listObjDuplicate */
/* global ListIssue */
+/* global mockBoardService */
import Vue from 'vue';
import Cookies from 'js-cookie';
@@ -20,7 +21,7 @@ import './mock_data';
describe('Store', () => {
beforeEach(() => {
Vue.http.interceptors.push(boardsMockInterceptor);
- gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
spyOn(gl.boardService, 'moveIssue').and.callFake(() => new Promise((resolve) => {
@@ -78,7 +79,7 @@ describe('Store', () => {
it('persists new list', (done) => {
gl.issueBoards.BoardsStore.new({
title: 'Test',
- type: 'label',
+ list_type: 'label',
label: {
id: 1,
title: 'Testing',
@@ -210,6 +211,7 @@ describe('Store', () => {
it('moves issue in list', (done) => {
const issue = new ListIssue({
title: 'Testing',
+ id: 2,
iid: 2,
confidential: false,
labels: [],
diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js
index c4e8966ad6c..8dacac20cad 100644
--- a/spec/javascripts/boards/components/board_spec.js
+++ b/spec/javascripts/boards/components/board_spec.js
@@ -1,7 +1,9 @@
+/* global mockBoardService */
import Vue from 'vue';
import '~/boards/services/board_service';
import '~/boards/components/board';
import '~/boards/models/list';
+import '../mock_data';
describe('Board component', () => {
let vm;
@@ -13,8 +15,12 @@ describe('Board component', () => {
el = document.createElement('div');
document.body.appendChild(el);
- // eslint-disable-next-line no-undef
- gl.boardService = new BoardService('/', '/', 1);
+ gl.boardService = mockBoardService({
+ boardsEndpoint: '/',
+ listsEndpoint: '/',
+ bulkUpdatePath: '/',
+ boardId: 1,
+ });
vm = new gl.issueBoards.Board({
propsData: {
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 69cfcbbce5a..7d430ec35e2 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -37,6 +37,7 @@ describe('Issue card component', () => {
list = listObj;
issue = new ListIssue({
title: 'Testing',
+ id: 1,
iid: 1,
confidential: false,
labels: [list.label],
@@ -238,46 +239,63 @@ describe('Issue card component', () => {
});
describe('labels', () => {
- describe('exists', () => {
- beforeEach((done) => {
- component.issue.addLabel(label1);
+ beforeEach((done) => {
+ component.issue.addLabel(label1);
- Vue.nextTick(() => done());
- });
+ Vue.nextTick(() => done());
+ });
- it('renders list label', () => {
- expect(
- component.$el.querySelectorAll('.label').length,
- ).toBe(2);
+ it('renders list label', () => {
+ expect(
+ component.$el.querySelectorAll('.label').length,
+ ).toBe(2);
+ });
+
+ it('renders label', () => {
+ const nodes = [];
+ component.$el.querySelectorAll('.label').forEach((label) => {
+ nodes.push(label.title);
});
- it('renders label', () => {
- const nodes = [];
- component.$el.querySelectorAll('.label').forEach((label) => {
- nodes.push(label.title);
- });
+ expect(
+ nodes.includes(label1.description),
+ ).toBe(true);
+ });
- expect(
- nodes.includes(label1.description),
- ).toBe(true);
- });
+ it('sets label description as title', () => {
+ expect(
+ component.$el.querySelector('.label').getAttribute('title'),
+ ).toContain(label1.description);
+ });
- it('sets label description as title', () => {
- expect(
- component.$el.querySelector('.label').getAttribute('title'),
- ).toContain(label1.description);
+ it('sets background color of button', () => {
+ const nodes = [];
+ component.$el.querySelectorAll('.label').forEach((label) => {
+ nodes.push(label.style.backgroundColor);
});
- it('sets background color of button', () => {
- const nodes = [];
- component.$el.querySelectorAll('.label').forEach((label) => {
- nodes.push(label.style.backgroundColor);
- });
+ expect(
+ nodes.includes(label1.color),
+ ).toBe(true);
+ });
- expect(
- nodes.includes(label1.color),
- ).toBe(true);
- });
+ it('does not render label if label does not have an ID', (done) => {
+ component.issue.addLabel(new ListLabel({
+ title: 'closed',
+ }));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(
+ component.$el.querySelectorAll('.label').length,
+ ).toBe(2);
+ expect(
+ component.$el.textContent,
+ ).not.toContain('closed');
+
+ done();
+ })
+ .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index cd1497bc5e6..022d286d5df 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -1,6 +1,7 @@
/* eslint-disable comma-dangle */
/* global BoardService */
/* global ListIssue */
+/* global mockBoardService */
import Vue from 'vue';
import '~/lib/utils/url_utility';
@@ -16,11 +17,12 @@ describe('Issue model', () => {
let issue;
beforeEach(() => {
- gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.boardService = mockBoardService();
gl.issueBoards.BoardsStore.create();
issue = new ListIssue({
title: 'Testing',
+ id: 1,
iid: 1,
confidential: false,
labels: [{
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index db50829a276..d4627223a12 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -1,6 +1,7 @@
/* eslint-disable comma-dangle */
/* global boardsMockInterceptor */
/* global BoardService */
+/* global mockBoardService */
/* global List */
/* global ListIssue */
/* global listObj */
@@ -22,7 +23,9 @@ describe('List model', () => {
beforeEach(() => {
Vue.http.interceptors.push(boardsMockInterceptor);
- gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.boardService = mockBoardService({
+ bulkUpdatePath: '/test/issue-boards/board/1/lists',
+ });
gl.issueBoards.BoardsStore.create();
list = new List(listObj);
@@ -92,6 +95,7 @@ describe('List model', () => {
const listDup = new List(listObjDuplicate);
const issue = new ListIssue({
title: 'Testing',
+ id: _.random(10000),
iid: _.random(10000),
confidential: false,
labels: [list.label, listDup.label],
@@ -118,6 +122,7 @@ describe('List model', () => {
for (let i = 0; i < 30; i += 1) {
list.issues.push(new ListIssue({
title: 'Testing',
+ id: _.random(10000) + i,
iid: _.random(10000) + i,
confidential: false,
labels: [list.label],
@@ -137,7 +142,7 @@ describe('List model', () => {
it('does not increase page number if issue count is less than the page size', () => {
list.issues.push(new ListIssue({
title: 'Testing',
- iid: _.random(10000),
+ id: _.random(10000),
confidential: false,
labels: [list.label],
assignees: [],
@@ -156,7 +161,7 @@ describe('List model', () => {
spyOn(gl.boardService, 'newIssue').and.returnValue(Promise.resolve({
json() {
return {
- iid: 42,
+ id: 42,
};
},
}));
@@ -165,14 +170,14 @@ describe('List model', () => {
it('adds new issue to top of list', (done) => {
list.issues.push(new ListIssue({
title: 'Testing',
- iid: _.random(10000),
+ id: _.random(10000),
confidential: false,
labels: [list.label],
assignees: [],
}));
const dummyIssue = new ListIssue({
title: 'new issue',
- iid: _.random(10000),
+ id: _.random(10000),
confidential: false,
labels: [list.label],
assignees: [],
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index a64c3964ee3..0a93086985e 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -1,3 +1,4 @@
+/* global BoardService */
/* eslint-disable comma-dangle, no-unused-vars, quote-props */
const listObj = {
@@ -28,19 +29,19 @@ const listObjDuplicate = {
const BoardsMockData = {
'GET': {
- '/test/issue-boards/board/1/lists{/id}/issues': {
+ '/test/boards/1{/id}/issues': {
issues: [{
title: 'Testing',
+ id: 1,
iid: 1,
confidential: false,
labels: [],
assignees: [],
}],
- size: 1
}
},
'POST': {
- '/test/issue-boards/board/1/lists{/id}': listObj
+ '/test/boards/1{/id}': listObj
},
'PUT': {
'/test/issue-boards/board/1/lists{/id}': {}
@@ -58,7 +59,22 @@ const boardsMockInterceptor = (request, next) => {
}));
};
+const mockBoardService = (opts = {}) => {
+ const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/board';
+ const listsEndpoint = opts.listsEndpoint || '/test/boards/1';
+ const bulkUpdatePath = opts.bulkUpdatePath || '';
+ const boardId = opts.boardId || '1';
+
+ return new BoardService({
+ boardsEndpoint,
+ listsEndpoint,
+ bulkUpdatePath,
+ boardId,
+ });
+};
+
window.listObj = listObj;
window.listObjDuplicate = listObjDuplicate;
window.BoardsMockData = BoardsMockData;
window.boardsMockInterceptor = boardsMockInterceptor;
+window.mockBoardService = mockBoardService;
diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js
index 32e6d04df9f..7eecb58a4c3 100644
--- a/spec/javascripts/boards/modal_store_spec.js
+++ b/spec/javascripts/boards/modal_store_spec.js
@@ -18,6 +18,7 @@ describe('Modal store', () => {
issue = new ListIssue({
title: 'Testing',
+ id: 1,
iid: 1,
confidential: false,
labels: [],
@@ -25,6 +26,7 @@ describe('Modal store', () => {
});
issue2 = new ListIssue({
title: 'Testing',
+ id: 1,
iid: 2,
confidential: false,
labels: [],
diff --git a/spec/javascripts/breakpoints_spec.js b/spec/javascripts/breakpoints_spec.js
new file mode 100644
index 00000000000..b1b5d36c1fb
--- /dev/null
+++ b/spec/javascripts/breakpoints_spec.js
@@ -0,0 +1,15 @@
+import bp, {
+ breakpoints,
+} from '~/breakpoints';
+
+describe('breakpoints', () => {
+ Object.keys(breakpoints).forEach((key) => {
+ const size = breakpoints[key];
+
+ it(`returns ${key} when larger than ${size}`, () => {
+ spyOn(bp, 'windowWidth').and.returnValue(size + 10);
+
+ expect(bp.getBreakpointSize()).toBe(key);
+ });
+ });
+});
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index a34cadec0ab..454f187ccbc 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -29,6 +29,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
});
@@ -64,6 +65,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
});
@@ -115,6 +117,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
element.appendChild(this.component.$el);
@@ -136,6 +139,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
});
diff --git a/spec/javascripts/droplab/drop_down_spec.js b/spec/javascripts/droplab/drop_down_spec.js
index 2bbcebeeac0..1ef494a00b8 100644
--- a/spec/javascripts/droplab/drop_down_spec.js
+++ b/spec/javascripts/droplab/drop_down_spec.js
@@ -351,14 +351,17 @@ describe('DropDown', function () {
describe('render', function () {
beforeEach(function () {
- this.list = { querySelector: () => {} };
+ this.list = { querySelector: () => {}, dispatchEvent: () => {} };
this.dropdown = { renderChildren: () => {}, list: this.list };
this.renderableList = {};
this.data = [0, 1];
+ this.customEvent = {};
spyOn(this.dropdown, 'renderChildren').and.callFake(data => data);
spyOn(this.list, 'querySelector').and.returnValue(this.renderableList);
+ spyOn(this.list, 'dispatchEvent');
spyOn(this.data, 'map').and.callThrough();
+ spyOn(window, 'CustomEvent').and.returnValue(this.customEvent);
DropDown.prototype.render.call(this.dropdown, this.data);
});
@@ -375,6 +378,14 @@ describe('DropDown', function () {
expect(this.renderableList.innerHTML).toBe('01');
});
+ it('should call render.dl', function () {
+ expect(window.CustomEvent).toHaveBeenCalledWith('render.dl', jasmine.any(Object));
+ });
+
+ it('should call dispatchEvent with the customEvent', function () {
+ expect(this.list.dispatchEvent).toHaveBeenCalledWith(this.customEvent);
+ });
+
describe('if no data argument is passed', function () {
beforeEach(function () {
this.data.map.calls.reset();
@@ -394,7 +405,7 @@ describe('DropDown', function () {
describe('if no dynamic list is present', function () {
beforeEach(function () {
- this.list = { querySelector: () => {} };
+ this.list = { querySelector: () => {}, dispatchEvent: () => {} };
this.dropdown = { renderChildren: () => {}, list: this.list };
this.data = [0, 1];
diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
new file mode 100644
index 00000000000..114d282e48a
--- /dev/null
+++ b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
@@ -0,0 +1,219 @@
+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
new file mode 100644
index 00000000000..7feb361edec
--- /dev/null
+++ b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js
@@ -0,0 +1,45 @@
+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
new file mode 100644
index 00000000000..6abe8425ee7
--- /dev/null
+++ b/spec/javascripts/feature_highlight/feature_highlight_spec.js
@@ -0,0 +1,122 @@
+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/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb
index de673f94d72..387858cba77 100644
--- a/spec/javascripts/fixtures/abuse_reports.rb
+++ b/spec/javascripts/fixtures/abuse_reports.rb
@@ -14,7 +14,7 @@ describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controll
clean_frontend_fixtures('abuse_reports/')
end
- before(:each) do
+ before do
sign_in(admin)
end
diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/blob.rb
new file mode 100644
index 00000000000..81e8a51a902
--- /dev/null
+++ b/spec/javascripts/fixtures/blob.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('blob/')
+ end
+
+ before do
+ sign_in(admin)
+ end
+
+ after do
+ remove_repository(project)
+ end
+
+ it 'blob/show.html.raw' do |example|
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: 'add-ipython-files/files/ipython/basic.ipynb')
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/boards.rb b/spec/javascripts/fixtures/boards.rb
index d7c3dc0a235..494c9cabdcc 100644
--- a/spec/javascripts/fixtures/boards.rb
+++ b/spec/javascripts/fixtures/boards.rb
@@ -13,7 +13,7 @@ describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller
clean_frontend_fixtures('boards/')
end
- before(:each) do
+ before do
sign_in(admin)
end
diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb
index a059818145b..4fc072d2585 100644
--- a/spec/javascripts/fixtures/branches.rb
+++ b/spec/javascripts/fixtures/branches.rb
@@ -13,10 +13,14 @@ describe Projects::BranchesController, '(JavaScript fixtures)', type: :controlle
clean_frontend_fixtures('branches/')
end
- before(:each) do
+ before do
sign_in(admin)
end
+ after do
+ remove_repository(project)
+ end
+
it 'branches/new_branch.html.raw' do |example|
get :new,
namespace_id: project.namespace.to_param,
diff --git a/spec/javascripts/fixtures/dashboard.rb b/spec/javascripts/fixtures/dashboard.rb
index e83db8daaf2..7fa351680c9 100644
--- a/spec/javascripts/fixtures/dashboard.rb
+++ b/spec/javascripts/fixtures/dashboard.rb
@@ -13,10 +13,14 @@ describe Dashboard::ProjectsController, '(JavaScript fixtures)', type: :controll
clean_frontend_fixtures('dashboard/')
end
- before(:each) do
+ 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)
diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb
index fca3f5b1bfe..580894ceaf9 100644
--- a/spec/javascripts/fixtures/deploy_keys.rb
+++ b/spec/javascripts/fixtures/deploy_keys.rb
@@ -12,10 +12,14 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control
clean_frontend_fixtures('deploy_keys/')
end
- before(:each) do
+ before do
sign_in(admin)
end
+ after do
+ remove_repository(project)
+ end
+
render_views
it 'deploy_keys/keys.json' do |example|
diff --git a/spec/javascripts/fixtures/environments.rb b/spec/javascripts/fixtures/environments.rb
index 3474f4696ef..d2457d75419 100644
--- a/spec/javascripts/fixtures/environments.rb
+++ b/spec/javascripts/fixtures/environments.rb
@@ -14,7 +14,7 @@ describe Projects::EnvironmentsController, '(JavaScript fixtures)', type: :contr
clean_frontend_fixtures('environments/metrics')
end
- before(:each) do
+ before do
sign_in(admin)
end
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
index 1a30909977e..0ee2f82dfd6 100644
--- a/spec/javascripts/fixtures/issues.rb
+++ b/spec/javascripts/fixtures/issues.rb
@@ -13,10 +13,14 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
clean_frontend_fixtures('issues/')
end
- before(:each) do
+ before do
sign_in(admin)
end
+ after do
+ remove_repository(project)
+ end
+
it 'issues/open-issue.html.raw' do |example|
render_issue(example.description, create(:issue, project: project))
end
diff --git a/spec/javascripts/fixtures/jobs.rb b/spec/javascripts/fixtures/jobs.rb
index dc7dde1138c..87d131dfe28 100644
--- a/spec/javascripts/fixtures/jobs.rb
+++ b/spec/javascripts/fixtures/jobs.rb
@@ -17,10 +17,14 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
clean_frontend_fixtures('builds/')
end
- before(:each) do
+ before do
sign_in(admin)
end
+ after do
+ remove_repository(project)
+ end
+
it 'builds/build-with-artifacts.html.raw' do |example|
get :show,
namespace_id: project.namespace.to_param,
diff --git a/spec/javascripts/fixtures/labels.rb b/spec/javascripts/fixtures/labels.rb
index 2e4811b64a4..b730d557e21 100644
--- a/spec/javascripts/fixtures/labels.rb
+++ b/spec/javascripts/fixtures/labels.rb
@@ -19,10 +19,14 @@ describe 'Labels (JavaScript fixtures)' do
clean_frontend_fixtures('labels/')
end
+ after do
+ remove_repository(project)
+ end
+
describe Groups::LabelsController, '(JavaScript fixtures)', type: :controller do
render_views
- before(:each) do
+ before do
sign_in(admin)
end
@@ -39,7 +43,7 @@ describe 'Labels (JavaScript fixtures)' do
describe Projects::LabelsController, '(JavaScript fixtures)', type: :controller do
render_views
- before(:each) do
+ before do
sign_in(admin)
end
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index f9d8b5c569c..4bc2205e642 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -33,10 +33,14 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
clean_frontend_fixtures('merge_requests/')
end
- before(:each) do
+ before do
sign_in(admin)
end
+ after do
+ remove_repository(project)
+ end
+
it 'merge_requests/merge_request_with_task_list.html.raw' do |example|
create(:ci_build, :pending, pipeline: pipeline)
@@ -55,6 +59,11 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
render_merge_request(example.description, merge_request)
end
+ it 'merge_requests/merge_request_with_comment.html.raw' do |example|
+ create(:note_on_merge_request, author: admin, project: project, noteable: merge_request, note: '- [ ] Task List Item')
+ render_merge_request(example.description, merge_request)
+ end
+
private
def render_merge_request(fixture_file_name, merge_request)
diff --git a/spec/javascripts/fixtures/merge_requests_diffs.rb b/spec/javascripts/fixtures/merge_requests_diffs.rb
index 4481a187f63..ddce00bc0fe 100644
--- a/spec/javascripts/fixtures/merge_requests_diffs.rb
+++ b/spec/javascripts/fixtures/merge_requests_diffs.rb
@@ -25,10 +25,14 @@ describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type
clean_frontend_fixtures('merge_request_diffs/')
end
- before(:each) do
+ before do
sign_in(admin)
end
+ after do
+ remove_repository(project)
+ end
+
it 'merge_request_diffs/inline_changes_tab_with_comments.json' do |example|
create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request)
create(:note_on_merge_request, author: admin, project: project, noteable: merge_request)
diff --git a/spec/javascripts/fixtures/pipelines.rb b/spec/javascripts/fixtures/pipelines.rb
index daafbac86db..bb85da50f0f 100644
--- a/spec/javascripts/fixtures/pipelines.rb
+++ b/spec/javascripts/fixtures/pipelines.rb
@@ -19,7 +19,7 @@ describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controll
clean_frontend_fixtures('pipelines/')
end
- before(:each) do
+ before do
sign_in(admin)
end
diff --git a/spec/javascripts/fixtures/project_select_combo_button.html.haml b/spec/javascripts/fixtures/project_select_combo_button.html.haml
index 54bc1a59279..432cd5fcc74 100644
--- a/spec/javascripts/fixtures/project_select_combo_button.html.haml
+++ b/spec/javascripts/fixtures/project_select_combo_button.html.haml
@@ -1,6 +1,6 @@
.project-item-select-holder
%input.project-item-select{ data: { group_id: '12345' , relative_path: 'issues/new' } }
- %a.new-project-item-link{ data: { label: 'New issue' }, href: ''}
+ %a.new-project-item-link{ data: { label: 'New issue', type: 'issues' }, href: ''}
%i.fa.fa-spinner.spin
%a.new-project-item-select-button
%i.fa.fa-caret-down
diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb
index 6c33b240e5c..2a100e7fab5 100644
--- a/spec/javascripts/fixtures/projects.rb
+++ b/spec/javascripts/fixtures/projects.rb
@@ -13,10 +13,14 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller do
clean_frontend_fixtures('projects/')
end
- before(:each) do
+ before do
sign_in(admin)
end
+ after do
+ remove_repository(project)
+ end
+
it 'projects/dashboard.html.raw' do |example|
get :show,
namespace_id: project.namespace.to_param,
diff --git a/spec/javascripts/fixtures/prometheus_service.rb b/spec/javascripts/fixtures/prometheus_service.rb
index 3200577b326..f95f8038ffb 100644
--- a/spec/javascripts/fixtures/prometheus_service.rb
+++ b/spec/javascripts/fixtures/prometheus_service.rb
@@ -7,17 +7,21 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') }
let!(:service) { create(:prometheus_service, project: project) }
-
+
render_views
before(:all) do
clean_frontend_fixtures('services/prometheus')
end
- before(:each) do
+ before do
sign_in(admin)
end
+ after do
+ remove_repository(project)
+ end
+
it 'services/prometheus/prometheus_service.html.raw' do |example|
get :edit,
namespace_id: namespace,
diff --git a/spec/javascripts/fixtures/raw.rb b/spec/javascripts/fixtures/raw.rb
index 25f5a3b0bb3..82770beb39b 100644
--- a/spec/javascripts/fixtures/raw.rb
+++ b/spec/javascripts/fixtures/raw.rb
@@ -10,6 +10,10 @@ describe 'Raw files', '(JavaScript fixtures)', type: :controller do
clean_frontend_fixtures('blob/notebook/')
end
+ after do
+ remove_repository(project)
+ end
+
it 'blob/notebook/basic.json' do |example|
blob = project.repository.blob_at('6d85bb69', 'files/ipython/basic.ipynb')
diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb
index 554451d1bbf..9280ed5a7f1 100644
--- a/spec/javascripts/fixtures/services.rb
+++ b/spec/javascripts/fixtures/services.rb
@@ -7,7 +7,6 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') }
let!(:service) { create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker') }
-
render_views
@@ -15,10 +14,14 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle
clean_frontend_fixtures('services/')
end
- before(:each) do
+ before do
sign_in(admin)
end
+ after do
+ remove_repository(project)
+ end
+
it 'services/edit_service.html.raw' do |example|
get :edit,
namespace_id: namespace,
diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb
index cc825c82190..fa97f352e31 100644
--- a/spec/javascripts/fixtures/snippet.rb
+++ b/spec/javascripts/fixtures/snippet.rb
@@ -14,10 +14,14 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do
clean_frontend_fixtures('snippets/')
end
- before(:each) do
+ before do
sign_in(admin)
end
+ after do
+ remove_repository(project)
+ end
+
it 'snippets/show.html.raw' do |example|
get(:show, id: snippet.to_param)
diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb
index a81ef8c5492..426b854fe8b 100644
--- a/spec/javascripts/fixtures/todos.rb
+++ b/spec/javascripts/fixtures/todos.rb
@@ -15,10 +15,14 @@ describe 'Todos (JavaScript fixtures)' do
clean_frontend_fixtures('todos/')
end
+ after do
+ remove_repository(project)
+ end
+
describe Dashboard::TodosController, '(JavaScript fixtures)', type: :controller do
render_views
- before(:each) do
+ before do
sign_in(admin)
end
@@ -33,7 +37,7 @@ describe 'Todos (JavaScript fixtures)' do
describe Projects::TodosController, '(JavaScript fixtures)', type: :controller do
render_views
- before(:each) do
+ before do
sign_in(admin)
end
diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js
index ab74f3e00ec..f8b37c0edde 100644
--- a/spec/javascripts/fly_out_nav_spec.js
+++ b/spec/javascripts/fly_out_nav_spec.js
@@ -1,10 +1,20 @@
-/* global bp */
import {
calculateTop,
- hideSubLevelItems,
showSubLevelItems,
canShowSubItems,
+ canShowActiveSubItems,
+ mouseEnterTopItems,
+ mouseLeaveTopItem,
+ getOpenMenu,
+ setOpenMenu,
+ mousePos,
+ getHideSubItemsInterval,
+ documentMouseMove,
+ getHeaderHeight,
+ setSidebar,
+ subItemsMouseLeave,
} from '~/fly_out_nav';
+import bp from '~/breakpoints';
describe('Fly out sidebar navigation', () => {
let el;
@@ -16,11 +26,16 @@ describe('Fly out sidebar navigation', () => {
document.body.appendChild(el);
spyOn(bp, 'getBreakpointSize').and.callFake(() => breakpointSize);
+
+ setOpenMenu(null);
});
afterEach(() => {
- el.remove();
+ document.body.innerHTML = '';
breakpointSize = 'lg';
+ mousePos.length = 0;
+
+ setSidebar(null);
});
describe('calculateTop', () => {
@@ -47,61 +62,152 @@ describe('Fly out sidebar navigation', () => {
});
});
- describe('hideSubLevelItems', () => {
+ describe('getHideSubItemsInterval', () => {
beforeEach(() => {
- el.innerHTML = '<div class="sidebar-sub-level-items"></div>';
+ el.innerHTML = '<div class="sidebar-sub-level-items" style="position: fixed; top: 0; left: 100px; height: 150px;"></div>';
});
- it('hides subitems', () => {
- hideSubLevelItems(el);
+ it('returns 0 if currentOpenMenu is nil', () => {
+ expect(
+ getHideSubItemsInterval(),
+ ).toBe(0);
+ });
+
+ it('returns 0 when mouse above sub-items', () => {
+ showSubLevelItems(el);
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left,
+ clientY: el.getBoundingClientRect().top,
+ });
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left,
+ clientY: el.getBoundingClientRect().top - 50,
+ });
expect(
- el.querySelector('.sidebar-sub-level-items').style.display,
- ).toBe('none');
+ getHideSubItemsInterval(),
+ ).toBe(0);
});
- it('does not hude subitems on mobile', () => {
- breakpointSize = 'sm';
+ it('returns 0 when mouse is below sub-items', () => {
+ const subItems = el.querySelector('.sidebar-sub-level-items');
- hideSubLevelItems(el);
+ showSubLevelItems(el);
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left,
+ clientY: el.getBoundingClientRect().top,
+ });
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left,
+ clientY: (el.getBoundingClientRect().top - subItems.getBoundingClientRect().height) + 50,
+ });
expect(
- el.querySelector('.sidebar-sub-level-items').style.display,
- ).not.toBe('none');
+ getHideSubItemsInterval(),
+ ).toBe(0);
+ });
+
+ it('returns 300 when mouse is moved towards sub-items', () => {
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left,
+ clientY: el.getBoundingClientRect().top,
+ });
+ showSubLevelItems(el);
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left + 20,
+ clientY: el.getBoundingClientRect().top + 10,
+ });
+
+ expect(
+ getHideSubItemsInterval(),
+ ).toBe(300);
});
+ });
- it('removes is-over class', () => {
+ describe('mouseLeaveTopItem', () => {
+ beforeEach(() => {
spyOn(el.classList, 'remove');
+ });
- hideSubLevelItems(el);
+ it('removes is-over class if currentOpenMenu is null', () => {
+ mouseLeaveTopItem(el);
expect(
el.classList.remove,
).toHaveBeenCalledWith('is-over');
});
- it('removes is-above class from sub-items', () => {
- const subItems = el.querySelector('.sidebar-sub-level-items');
+ it('removes is-over class if currentOpenMenu is null & there are sub-items', () => {
+ el.innerHTML = '<div class="sidebar-sub-level-items" style="position: absolute;"></div>';
- spyOn(subItems.classList, 'remove');
+ mouseLeaveTopItem(el);
- hideSubLevelItems(el);
+ expect(
+ el.classList.remove,
+ ).toHaveBeenCalledWith('is-over');
+ });
+
+ it('does not remove is-over class if currentOpenMenu is the passed in sub-items', () => {
+ el.innerHTML = '<div class="sidebar-sub-level-items" style="position: absolute;"></div>';
+
+ setOpenMenu(el.querySelector('.sidebar-sub-level-items'));
+ mouseLeaveTopItem(el);
expect(
- subItems.classList.remove,
- ).toHaveBeenCalledWith('is-above');
+ el.classList.remove,
+ ).not.toHaveBeenCalled();
});
+ });
- it('does nothing if el has no sub-items', () => {
- el.innerHTML = '';
+ describe('mouseEnterTopItems', () => {
+ beforeEach(() => {
+ jasmine.clock().install();
- spyOn(el.classList, 'remove');
+ el.innerHTML = '<div class="sidebar-sub-level-items" style="position: absolute; top: 0; left: 100px; height: 200px;"></div>';
+ });
- hideSubLevelItems(el);
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
+ it('shows sub-items after 0ms if no menu is open', () => {
+ mouseEnterTopItems(el);
expect(
- el.classList.remove,
- ).not.toHaveBeenCalledWith();
+ getHideSubItemsInterval(),
+ ).toBe(0);
+
+ jasmine.clock().tick(0);
+
+ expect(
+ el.querySelector('.sidebar-sub-level-items').style.display,
+ ).toBe('block');
+ });
+
+ it('shows sub-items after 300ms if a menu is currently open', () => {
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left,
+ clientY: el.getBoundingClientRect().top,
+ });
+
+ setOpenMenu(el.querySelector('.sidebar-sub-level-items'));
+
+ documentMouseMove({
+ clientX: el.getBoundingClientRect().left + 20,
+ clientY: el.getBoundingClientRect().top + 10,
+ });
+
+ mouseEnterTopItems(el);
+
+ expect(
+ getHideSubItemsInterval(),
+ ).toBe(300);
+
+ jasmine.clock().tick(300);
+
+ expect(
+ el.querySelector('.sidebar-sub-level-items').style.display,
+ ).toBe('block');
});
});
@@ -121,7 +227,7 @@ describe('Fly out sidebar navigation', () => {
});
it('does not show sub-items on mobile', () => {
- breakpointSize = 'sm';
+ breakpointSize = 'xs';
showSubLevelItems(el);
@@ -130,7 +236,7 @@ describe('Fly out sidebar navigation', () => {
).not.toBe('block');
});
- it('does not shows sub-items', () => {
+ it('shows sub-items', () => {
showSubLevelItems(el);
expect(
@@ -138,13 +244,39 @@ describe('Fly out sidebar navigation', () => {
).toBe('block');
});
+ it('shows collapsed only sub-items if icon only sidebar', () => {
+ const subItems = el.querySelector('.sidebar-sub-level-items');
+ const sidebar = document.createElement('div');
+ sidebar.classList.add('sidebar-icons-only');
+ subItems.classList.add('is-fly-out-only');
+
+ setSidebar(sidebar);
+
+ showSubLevelItems(el);
+
+ expect(
+ el.querySelector('.sidebar-sub-level-items').style.display,
+ ).toBe('block');
+ });
+
+ it('does not show collapsed only sub-items if icon only sidebar', () => {
+ const subItems = el.querySelector('.sidebar-sub-level-items');
+ subItems.classList.add('is-fly-out-only');
+
+ showSubLevelItems(el);
+
+ expect(
+ subItems.style.display,
+ ).not.toBe('block');
+ });
+
it('sets transform of sub-items', () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
showSubLevelItems(el);
expect(
subItems.style.transform,
- ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top)}px, 0px)`);
+ ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`);
});
it('sets is-above when element is above', () => {
@@ -170,11 +302,67 @@ describe('Fly out sidebar navigation', () => {
});
it('returns false if on mobile size', () => {
- breakpointSize = 'sm';
+ breakpointSize = 'xs';
expect(
canShowSubItems(),
).toBeFalsy();
});
});
+
+ describe('canShowActiveSubItems', () => {
+ it('returns true by default', () => {
+ expect(
+ canShowActiveSubItems(el),
+ ).toBeTruthy();
+ });
+
+ it('returns false when active & expanded sidebar', () => {
+ const sidebar = document.createElement('div');
+ el.classList.add('active');
+
+ setSidebar(sidebar);
+
+ expect(
+ canShowActiveSubItems(el),
+ ).toBeFalsy();
+ });
+
+ it('returns true when active & collapsed sidebar', () => {
+ const sidebar = document.createElement('div');
+ sidebar.classList.add('sidebar-icons-only');
+ el.classList.add('active');
+
+ setSidebar(sidebar);
+
+ expect(
+ canShowActiveSubItems(el),
+ ).toBeTruthy();
+ });
+ });
+
+ describe('subItemsMouseLeave', () => {
+ beforeEach(() => {
+ el.innerHTML = '<div class="sidebar-sub-level-items" style="position: absolute;"></div>';
+
+ setOpenMenu(el.querySelector('.sidebar-sub-level-items'));
+ });
+
+ it('hides subMenu if element is not hovered', () => {
+ subItemsMouseLeave(el);
+
+ expect(
+ getOpenMenu(),
+ ).toBeNull();
+ });
+
+ it('does not hide subMenu if element is hovered', () => {
+ el.classList.add('is-over');
+ subItemsMouseLeave(el);
+
+ expect(
+ getOpenMenu(),
+ ).not.toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 10fcc590c89..ca048123bf7 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -4,8 +4,11 @@ import '~/gl_dropdown';
import '~/lib/utils/common_utils';
import '~/lib/utils/url_utility';
-(() => {
- const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+describe('glDropdown', function describeDropdown() {
+ preloadFixtures('static/gl_dropdown.html.raw');
+ loadJSONFixtures('projects.json');
+
+ const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
@@ -39,187 +42,217 @@ import '~/lib/utils/url_utility';
remoteCallback = callback.bind({}, data);
};
- describe('Dropdown', function describeDropdown() {
- preloadFixtures('static/gl_dropdown.html.raw');
- loadJSONFixtures('projects.json');
-
- function initDropDown(hasRemote, isFilterable, extraOpts = {}) {
- const options = Object.assign({
- selectable: true,
- filterable: isFilterable,
- data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
- search: {
- fields: ['name']
- },
- text: project => (project.name_with_namespace || project.name),
- id: project => project.id,
- }, extraOpts);
- this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options);
- }
+ function initDropDown(hasRemote, isFilterable, extraOpts = {}) {
+ const options = Object.assign({
+ selectable: true,
+ filterable: isFilterable,
+ data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
+ search: {
+ fields: ['name']
+ },
+ text: project => (project.name_with_namespace || project.name),
+ id: project => project.id,
+ }, extraOpts);
+ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown(options);
+ }
+
+ beforeEach(() => {
+ loadFixtures('static/gl_dropdown.html.raw');
+ this.dropdownContainerElement = $('.dropdown.inline');
+ this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+ this.projectsData = getJSONFixture('projects.json');
+ });
- beforeEach(() => {
- loadFixtures('static/gl_dropdown.html.raw');
- this.dropdownContainerElement = $('.dropdown.inline');
- this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
- this.projectsData = getJSONFixture('projects.json');
- });
+ afterEach(() => {
+ $('body').unbind('keydown');
+ this.dropdownContainerElement.unbind('keyup');
+ });
- afterEach(() => {
- $('body').unbind('keydown');
- this.dropdownContainerElement.unbind('keyup');
- });
+ it('should open on click', () => {
+ initDropDown.call(this, false);
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ this.dropdownButtonElement.click();
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ });
- it('should open on click', () => {
- initDropDown.call(this, false);
- expect(this.dropdownContainerElement).not.toHaveClass('open');
- this.dropdownButtonElement.click();
- expect(this.dropdownContainerElement).toHaveClass('open');
- });
+ it('escapes HTML as text', () => {
+ this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>';
- it('escapes HTML as text', () => {
- this.projectsData[0].name_with_namespace = '<script>alert("testing");</script>';
+ initDropDown.call(this, false);
- initDropDown.call(this, false);
+ this.dropdownButtonElement.click();
- this.dropdownButtonElement.click();
+ expect(
+ $('.dropdown-content li:first-child').text(),
+ ).toBe('<script>alert("testing");</script>');
+ });
- expect(
- $('.dropdown-content li:first-child').text(),
- ).toBe('<script>alert("testing");</script>');
- });
+ it('should output HTML when highlighting', () => {
+ this.projectsData[0].name_with_namespace = 'testing';
+ $('.dropdown-input .dropdown-input-field').val('test');
- it('should output HTML when highlighting', () => {
- this.projectsData[0].name_with_namespace = 'testing';
- $('.dropdown-input .dropdown-input-field').val('test');
+ initDropDown.call(this, false, true, {
+ highlight: true,
+ });
- initDropDown.call(this, false, true, {
- highlight: true,
- });
+ this.dropdownButtonElement.click();
- this.dropdownButtonElement.click();
+ expect(
+ $('.dropdown-content li:first-child').text(),
+ ).toBe('testing');
- expect(
- $('.dropdown-content li:first-child').text(),
- ).toBe('testing');
+ expect(
+ $('.dropdown-content li:first-child a').html(),
+ ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing');
+ });
- expect(
- $('.dropdown-content li:first-child a').html(),
- ).toBe('<b>t</b><b>e</b><b>s</b><b>t</b>ing');
+ describe('that is open', () => {
+ beforeEach(() => {
+ initDropDown.call(this, false, false);
+ this.dropdownButtonElement.click();
});
- describe('that is open', () => {
- beforeEach(() => {
- initDropDown.call(this, false, false);
- this.dropdownButtonElement.click();
+ it('should select a following item on DOWN keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
+ const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+ navigateWithKeys('down', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
});
+ });
- it('should select a following item on DOWN keypress', () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
- const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
- navigateWithKeys('down', randomIndex, () => {
+ it('should select a previous item on UP keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
+ navigateWithKeys('down', (this.projectsData.length - 1), () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+ navigateWithKeys('up', randomIndex, () => {
expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
+ expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
});
});
+ });
- it('should select a previous item on UP keypress', () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
- navigateWithKeys('down', (this.projectsData.length - 1), () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
- navigateWithKeys('up', randomIndex, () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
- expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
- });
+ it('should click the selected item on ENTER keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
+ navigateWithKeys('down', randomIndex, () => {
+ spyOn(gl.utils, 'visitUrl').and.stub();
+ navigateWithKeys('enter', null, () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
+ expect(link).toHaveClass('is-active');
+ const linkedLocation = link.attr('href');
+ if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);
});
});
+ });
- it('should click the selected item on ENTER keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open');
- const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
- navigateWithKeys('down', randomIndex, () => {
- spyOn(gl.utils, 'visitUrl').and.stub();
- navigateWithKeys('enter', null, () => {
- expect(this.dropdownContainerElement).not.toHaveClass('open');
- const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
- expect(link).toHaveClass('is-active');
- const linkedLocation = link.attr('href');
- if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);
- });
- });
+ it('should close on ESC keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
});
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ });
+ });
- it('should close on ESC keypress', () => {
- expect(this.dropdownContainerElement).toHaveClass('open');
- this.dropdownContainerElement.trigger({
- type: 'keyup',
- which: ARROW_KEYS.ESC,
- keyCode: ARROW_KEYS.ESC
- });
- expect(this.dropdownContainerElement).not.toHaveClass('open');
+ describe('opened and waiting for a remote callback', () => {
+ beforeEach(() => {
+ initDropDown.call(this, true, true);
+ this.dropdownButtonElement.click();
+ });
+
+ it('should show loading indicator while search results are being fetched by backend', () => {
+ const dropdownMenu = document.querySelector('.dropdown-menu');
+
+ expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true);
+ remoteCallback();
+ expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false);
+ });
+
+ it('should not focus search input while remote task is not complete', () => {
+ expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
+ remoteCallback();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+
+ it('should focus search input after remote task is complete', () => {
+ remoteCallback();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+
+ it('should focus on input when opening for the second time after transition', () => {
+ remoteCallback();
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
});
+ this.dropdownButtonElement.click();
+ this.dropdownContainerElement.trigger('transitionend');
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
});
+ });
+
+ describe('input focus with array data', () => {
+ it('should focus input when passing array data to drop down', () => {
+ initDropDown.call(this, false, true);
+ this.dropdownButtonElement.click();
+ this.dropdownContainerElement.trigger('transitionend');
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+ });
+
+ it('should still have input value on close and restore', () => {
+ const $searchInput = $(SEARCH_INPUT_SELECTOR);
+ initDropDown.call(this, false, true);
+ $searchInput
+ .trigger('focus')
+ .val('g')
+ .trigger('input');
+ expect($searchInput.val()).toEqual('g');
+ this.dropdownButtonElement.trigger('hidden.bs.dropdown');
+ $searchInput
+ .trigger('blur')
+ .trigger('focus');
+ expect($searchInput.val()).toEqual('g');
+ });
+
+ describe('renderItem', () => {
+ describe('without selected value', () => {
+ let dropdown;
- describe('opened and waiting for a remote callback', () => {
beforeEach(() => {
- initDropDown.call(this, true, true);
- this.dropdownButtonElement.click();
+ const dropdownOptions = {
+
+ };
+ const $dropdownDiv = $('<div />');
+ $dropdownDiv.glDropdown(dropdownOptions);
+ dropdown = $dropdownDiv.data('glDropdown');
});
- it('should show loading indicator while search results are being fetched by backend', () => {
- const dropdownMenu = document.querySelector('.dropdown-menu');
+ it('marks items without ID as active', () => {
+ const dummyData = { };
- expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(true);
- remoteCallback();
- expect(dropdownMenu.className.indexOf('is-loading') !== -1).toEqual(false);
- });
+ const html = dropdown.renderItem(dummyData, null, null);
- it('should not focus search input while remote task is not complete', () => {
- expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
- remoteCallback();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ const link = html.querySelector('a');
+ expect(link).toHaveClass('is-active');
});
- it('should focus search input after remote task is complete', () => {
- remoteCallback();
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
- });
+ it('does not mark items with ID as active', () => {
+ const dummyData = {
+ id: 'ea'
+ };
- it('should focus on input when opening for the second time after transition', () => {
- remoteCallback();
- this.dropdownContainerElement.trigger({
- type: 'keyup',
- which: ARROW_KEYS.ESC,
- keyCode: ARROW_KEYS.ESC
- });
- this.dropdownButtonElement.click();
- this.dropdownContainerElement.trigger('transitionend');
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
- });
- });
+ const html = dropdown.renderItem(dummyData, null, null);
- describe('input focus with array data', () => {
- it('should focus input when passing array data to drop down', () => {
- initDropDown.call(this, false, true);
- this.dropdownButtonElement.click();
- this.dropdownContainerElement.trigger('transitionend');
- expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ const link = html.querySelector('a');
+ expect(link).not.toHaveClass('is-active');
});
});
-
- it('should still have input value on close and restore', () => {
- const $searchInput = $(SEARCH_INPUT_SELECTOR);
- initDropDown.call(this, false, true);
- $searchInput
- .trigger('focus')
- .val('g')
- .trigger('input');
- expect($searchInput.val()).toEqual('g');
- this.dropdownButtonElement.trigger('hidden.bs.dropdown');
- $searchInput
- .trigger('blur')
- .trigger('focus');
- expect($searchInput.val()).toEqual('g');
- });
});
-})();
+});
diff --git a/spec/javascripts/gpg_badges_spec.js b/spec/javascripts/gpg_badges_spec.js
new file mode 100644
index 00000000000..7a826487bf9
--- /dev/null
+++ b/spec/javascripts/gpg_badges_spec.js
@@ -0,0 +1,48 @@
+import GpgBadges from '~/gpg_badges';
+
+describe('GpgBadges', () => {
+ const dummyCommitSha = 'n0m0rec0ffee';
+ const dummyBadgeHtml = 'dummy html';
+ const dummyResponse = {
+ signatures: [{
+ commit_sha: dummyCommitSha,
+ html: dummyBadgeHtml,
+ }],
+ };
+
+ beforeEach(() => {
+ setFixtures(`
+ <div class="parent-container">
+ <div class="js-loading-gpg-badge" data-commit-sha="${dummyCommitSha}"></div>
+ </div>
+ `);
+ });
+
+ it('displays a loading spinner', () => {
+ spyOn($, 'get').and.returnValue({
+ done() {
+ // intentionally left blank
+ },
+ });
+
+ GpgBadges.fetch();
+
+ expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null);
+ const spinners = document.querySelectorAll('.js-loading-gpg-badge i.fa.fa-spinner.fa-spin');
+ expect(spinners.length).toBe(1);
+ });
+
+ it('replaces the loading spinner', () => {
+ spyOn($, 'get').and.returnValue({
+ done(callback) {
+ callback(dummyResponse);
+ },
+ });
+
+ GpgBadges.fetch();
+
+ expect(document.querySelector('.js-loading-gpg-badge')).toBe(null);
+ const parentContainer = document.querySelector('.parent-container');
+ expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml);
+ });
+});
diff --git a/spec/javascripts/groups/group_identicon_spec.js b/spec/javascripts/groups/group_identicon_spec.js
deleted file mode 100644
index 66772327503..00000000000
--- a/spec/javascripts/groups/group_identicon_spec.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import Vue from 'vue';
-import groupIdenticonComponent from '~/groups/components/group_identicon.vue';
-import GroupsStore from '~/groups/stores/groups_store';
-import { group1 } from './mock_data';
-
-const createComponent = () => {
- const Component = Vue.extend(groupIdenticonComponent);
- const store = new GroupsStore();
- const group = store.decorateGroup(group1);
-
- return new Component({
- propsData: {
- entityId: group.id,
- entityName: group.name,
- },
- }).$mount();
-};
-
-describe('GroupIdenticonComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- describe('computed', () => {
- describe('identiconStyles', () => {
- it('should return styles attribute value with `background-color` property', () => {
- vm.entityId = 4;
-
- expect(vm.identiconStyles).toBeDefined();
- expect(vm.identiconStyles.indexOf('background-color: #E0F2F1;') > -1).toBeTruthy();
- });
-
- it('should return styles attribute value with `color` property', () => {
- vm.entityId = 4;
-
- expect(vm.identiconStyles).toBeDefined();
- expect(vm.identiconStyles.indexOf('color: #555;') > -1).toBeTruthy();
- });
- });
-
- describe('identiconTitle', () => {
- it('should return first letter of entity title in uppercase', () => {
- vm.entityName = 'dummy-group';
-
- expect(vm.identiconTitle).toBeDefined();
- expect(vm.identiconTitle).toBe('D');
- });
- });
- });
-
- describe('template', () => {
- it('should render identicon', () => {
- expect(vm.$el.nodeName).toBe('DIV');
- expect(vm.$el.classList.contains('identicon')).toBeTruthy();
- expect(vm.$el.getAttribute('style').indexOf('background-color') > -1).toBeTruthy();
- });
- });
-});
diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js
new file mode 100644
index 00000000000..d7a2e86771c
--- /dev/null
+++ b/spec/javascripts/helpers/vue_mount_component_helper.js
@@ -0,0 +1,4 @@
+export default (Component, props = {}) => new Component({
+ propsData: props,
+}).$mount();
+
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 81ce18bf2fb..39065814bc2 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -34,16 +34,14 @@ describe('Issuable output', () => {
propsData: {
canUpdate: true,
canDestroy: true,
- canMove: true,
endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes',
issuableRef: '#1',
initialTitleHtml: '',
initialTitleText: '',
initialDescriptionHtml: '',
initialDescriptionText: '',
- markdownPreviewUrl: '/',
- markdownDocs: '/',
- projectsAutocompleteUrl: '/',
+ markdownPreviewPath: '/',
+ markdownDocsPath: '/',
isConfidential: false,
projectNamespace: '/',
projectPath: '/',
@@ -226,7 +224,7 @@ describe('Issuable output', () => {
});
});
- it('redirects if issue is moved', (done) => {
+ it('redirects if returned web_url has changed', (done) => {
spyOn(gl.utils, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
@@ -250,23 +248,6 @@ describe('Issuable output', () => {
});
});
- it('does not update issuable if project move confirm is false', (done) => {
- spyOn(window, 'confirm').and.returnValue(false);
- spyOn(vm.service, 'updateIssuable');
-
- vm.store.formState.move_to_project_id = 1;
-
- vm.updateIssuable();
-
- setTimeout(() => {
- expect(
- vm.service.updateIssuable,
- ).not.toHaveBeenCalled();
-
- done();
- });
- });
-
it('closes form on error', (done) => {
spyOn(window, 'Flash').and.callThrough();
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve, reject) => {
diff --git a/spec/javascripts/issue_show/components/edited_spec.js b/spec/javascripts/issue_show/components/edited_spec.js
index a0d0750ae34..2061def699b 100644
--- a/spec/javascripts/issue_show/components/edited_spec.js
+++ b/spec/javascripts/issue_show/components/edited_spec.js
@@ -46,4 +46,14 @@ describe('edited', () => {
expect(editedComponent.$el.querySelector('.author_link')).toBeFalsy();
expect(editedComponent.$el.querySelector('time')).toBeTruthy();
});
+
+ it('renders time ago tooltip at the bottom', () => {
+ const editedComponent = new EditedComponent({
+ propsData: {
+ updatedAt: '2017-05-15T12:31:04.428Z',
+ },
+ }).$mount();
+
+ expect(editedComponent.$el.querySelector('time').dataset.placement).toEqual('bottom');
+ });
});
diff --git a/spec/javascripts/issue_show/components/fields/description_spec.js b/spec/javascripts/issue_show/components/fields/description_spec.js
index df8189d9290..299f88e7778 100644
--- a/spec/javascripts/issue_show/components/fields/description_spec.js
+++ b/spec/javascripts/issue_show/components/fields/description_spec.js
@@ -25,8 +25,8 @@ describe('Description field component', () => {
vm = new Component({
el,
propsData: {
- markdownPreviewUrl: '/',
- markdownDocs: '/',
+ markdownPreviewPath: '/',
+ markdownDocsPath: '/',
formState: store.formState,
},
}).$mount();
diff --git a/spec/javascripts/issue_show/components/fields/project_move_spec.js b/spec/javascripts/issue_show/components/fields/project_move_spec.js
deleted file mode 100644
index 86d35c33ff4..00000000000
--- a/spec/javascripts/issue_show/components/fields/project_move_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Vue from 'vue';
-import projectMove from '~/issue_show/components/fields/project_move.vue';
-
-describe('Project move field component', () => {
- let vm;
- let formState;
-
- beforeEach((done) => {
- const Component = Vue.extend(projectMove);
-
- formState = {
- move_to_project_id: 0,
- };
-
- vm = new Component({
- propsData: {
- formState,
- projectsAutocompleteUrl: '/autocomplete',
- },
- }).$mount();
-
- Vue.nextTick(done);
- });
-
- it('mounts select2 element', () => {
- expect(
- vm.$el.querySelector('.select2-container'),
- ).not.toBeNull();
- });
-
- it('updates formState on change', () => {
- $(vm.$refs['move-dropdown']).val(2).trigger('change');
-
- expect(
- formState.move_to_project_id,
- ).toBe(2);
- });
-});
diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js
index 9a85223208c..6e89528a3ea 100644
--- a/spec/javascripts/issue_show/components/form_spec.js
+++ b/spec/javascripts/issue_show/components/form_spec.js
@@ -12,15 +12,13 @@ describe('Inline edit form component', () => {
vm = new Component({
propsData: {
canDestroy: true,
- canMove: true,
formState: {
title: 'b',
description: 'a',
lockedWarningVisible: false,
},
- markdownPreviewUrl: '/',
- markdownDocs: '/',
- projectsAutocompleteUrl: '/',
+ markdownPreviewPath: '/',
+ markdownDocsPath: '/',
projectPath: '/',
projectNamespace: '/',
},
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 0c8c4d2cea6..60a452f2223 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -118,7 +118,7 @@ describe('Issue', function() {
this.$triggeredButton = $btn;
- this.$projectIssuesCounter = $('.issue_counter');
+ this.$projectIssuesCounter = $('.issue_counter').first();
this.$projectIssuesCounter.text('1,001');
this.issueStateDeferred = new jQuery.Deferred();
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 55037bbbf73..a6ad250bd86 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -266,6 +266,12 @@ import '~/lib/utils/common_utils';
});
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('solves the promise from the callback', (done) => {
const expectedResponseValue = 'Success!';
gl.utils.backOff((next, stop) => (
@@ -299,37 +305,33 @@ import '~/lib/utils/common_utils';
let numberOfCalls = 1;
const expectedResponseValue = 'Success!';
gl.utils.backOff((next, stop) => (
- new Promise((resolve) => {
- resolve(expectedResponseValue);
- }).then((resp) => {
- if (numberOfCalls < 3) {
- numberOfCalls += 1;
- next();
- } else {
- stop(resp);
- }
- })
+ 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);
- expect(numberOfCalls).toBe(3);
done();
});
- }, 10000);
+ });
it('rejects the backOff promise after timing out', (done) => {
- const expectedResponseValue = 'Success!';
- gl.utils.backOff(next => (
- new Promise((resolve) => {
- resolve(expectedResponseValue);
- }).then(() => {
- setTimeout(next(), 5000); // it will time out
- })
- ), 3000).catch((errBackoffResp) => {
- expect(errBackoffResp instanceof Error).toBe(true);
- expect(errBackoffResp.message).toBe('BACKOFF_TIMEOUT');
- done();
- });
- }, 10000);
+ 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();
+ });
+ });
});
describe('gl.utils.setFavicon', () => {
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index ca1b1b7cc3c..f1a975ba962 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -52,6 +52,7 @@ describe('text_utility', () => {
beforeAll(() => {
textArea = document.createElement('textarea');
document.querySelector('body').appendChild(textArea);
+ textArea.focus();
});
afterAll(() => {
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index dc40244c20e..8830a2d29e5 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -295,6 +295,17 @@ import 'vendor/jquery.scrollTo';
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
});
+ it('triggers scroll event when diff already loaded', function () {
+ spyOn(document, 'dispatchEvent');
+
+ this.class.diffsLoaded = true;
+ this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+
+ expect(
+ document.dispatchEvent,
+ ).toHaveBeenCalledWith(new CustomEvent('scroll'));
+ });
+
describe('with inline diff', () => {
let noteId;
let noteLineNumId;
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
new file mode 100644
index 00000000000..752fdfb4614
--- /dev/null
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import Dashboard from '~/monitoring/components/dashboard.vue';
+import { MonitorMockInterceptor } from './mock_data';
+
+describe('Dashboard', () => {
+ const fixtureName = 'environments/metrics/metrics.html.raw';
+ let DashboardComponent;
+ let component;
+ preloadFixtures(fixtureName);
+
+ beforeEach(() => {
+ loadFixtures(fixtureName);
+ DashboardComponent = Vue.extend(Dashboard);
+ });
+
+ describe('no metrics are available yet', () => {
+ it('shows a getting started empty state when no metrics are present', () => {
+ component = new DashboardComponent({
+ el: document.querySelector('#prometheus-graphs'),
+ });
+
+ component.$mount();
+ expect(component.$el.querySelector('#prometheus-graphs')).toBe(null);
+ expect(component.state).toEqual('gettingStarted');
+ });
+ });
+
+ describe('requests information to the server', () => {
+ beforeEach(() => {
+ document.querySelector('#prometheus-graphs').setAttribute('data-has-metrics', 'true');
+ Vue.http.interceptors.push(MonitorMockInterceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, MonitorMockInterceptor);
+ });
+
+ it('shows up a loading state', (done) => {
+ component = new DashboardComponent({
+ el: document.querySelector('#prometheus-graphs'),
+ });
+ component.$mount();
+ Vue.nextTick(() => {
+ expect(component.state).toEqual('loading');
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/dashboard_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js
new file mode 100644
index 00000000000..e8f7042e131
--- /dev/null
+++ b/spec/javascripts/monitoring/dashboard_state_spec.js
@@ -0,0 +1,110 @@
+import Vue from 'vue';
+import EmptyState from '~/monitoring/components/empty_state.vue';
+import { statePaths } from './mock_data';
+
+const createComponent = (propsData) => {
+ const Component = Vue.extend(EmptyState);
+
+ return new Component({
+ propsData,
+ }).$mount();
+};
+
+function getTextFromNode(component, selector) {
+ return component.$el.querySelector(selector).firstChild.nodeValue.trim();
+}
+
+describe('EmptyState', () => {
+ describe('Computed props', () => {
+ it('currentState', () => {
+ const component = createComponent({
+ selectedState: 'gettingStarted',
+ settingsPath: statePaths.settingsPath,
+ documentationPath: statePaths.documentationPath,
+ });
+
+ expect(component.currentState).toBe(component.states.gettingStarted);
+ });
+
+ it('buttonPath returns settings path for the state "gettingStarted"', () => {
+ const component = createComponent({
+ selectedState: 'gettingStarted',
+ settingsPath: statePaths.settingsPath,
+ documentationPath: statePaths.documentationPath,
+ });
+
+ expect(component.buttonPath).toEqual(statePaths.settingsPath);
+ expect(component.buttonPath).not.toEqual(statePaths.documentationPath);
+ });
+
+ it('buttonPath returns documentation path for any of the other states', () => {
+ const component = createComponent({
+ selectedState: 'loading',
+ settingsPath: statePaths.settingsPath,
+ documentationPath: statePaths.documentationPath,
+ });
+
+ expect(component.buttonPath).toEqual(statePaths.documentationPath);
+ expect(component.buttonPath).not.toEqual(statePaths.settingsPath);
+ });
+
+ it('showButtonDescription returns a description with a link for the unableToConnect state', () => {
+ const component = createComponent({
+ selectedState: 'unableToConnect',
+ settingsPath: statePaths.settingsPath,
+ documentationPath: statePaths.documentationPath,
+ });
+
+ expect(component.showButtonDescription).toEqual(true);
+ });
+
+ it('showButtonDescription returns the description without a link for any other state', () => {
+ const component = createComponent({
+ selectedState: 'loading',
+ settingsPath: statePaths.settingsPath,
+ documentationPath: statePaths.documentationPath,
+ });
+
+ expect(component.showButtonDescription).toEqual(false);
+ });
+ });
+
+ it('should show the gettingStarted state', () => {
+ const component = createComponent({
+ selectedState: 'gettingStarted',
+ settingsPath: statePaths.settingsPath,
+ documentationPath: statePaths.documentationPath,
+ });
+
+ expect(component.$el.querySelector('svg')).toBeDefined();
+ expect(getTextFromNode(component, '.state-title')).toEqual(component.states.gettingStarted.title);
+ expect(getTextFromNode(component, '.state-description')).toEqual(component.states.gettingStarted.description);
+ expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.gettingStarted.buttonText);
+ });
+
+ it('should show the loading state', () => {
+ const component = createComponent({
+ selectedState: 'loading',
+ settingsPath: statePaths.settingsPath,
+ documentationPath: statePaths.documentationPath,
+ });
+
+ expect(component.$el.querySelector('svg')).toBeDefined();
+ expect(getTextFromNode(component, '.state-title')).toEqual(component.states.loading.title);
+ expect(getTextFromNode(component, '.state-description')).toEqual(component.states.loading.description);
+ expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.loading.buttonText);
+ });
+
+ it('should show the unableToConnect state', () => {
+ const component = createComponent({
+ selectedState: 'unableToConnect',
+ settingsPath: statePaths.settingsPath,
+ documentationPath: statePaths.documentationPath,
+ });
+
+ expect(component.$el.querySelector('svg')).toBeDefined();
+ expect(getTextFromNode(component, '.state-title')).toEqual(component.states.unableToConnect.title);
+ expect(component.$el.querySelector('.state-description a')).toBeDefined();
+ expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.unableToConnect.buttonText);
+ });
+});
diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js
new file mode 100644
index 00000000000..c2ff38ffab9
--- /dev/null
+++ b/spec/javascripts/monitoring/graph/deployment_spec.js
@@ -0,0 +1,137 @@
+import Vue from 'vue';
+import GraphDeployment from '~/monitoring/components/graph/deployment.vue';
+import { deploymentData } from '../mock_data';
+
+const createComponent = (propsData) => {
+ const Component = Vue.extend(GraphDeployment);
+
+ return new Component({
+ propsData,
+ }).$mount();
+};
+
+describe('MonitoringDeployment', () => {
+ const reducedDeploymentData = [deploymentData[0]];
+ reducedDeploymentData[0].ref = reducedDeploymentData[0].ref.name;
+ reducedDeploymentData[0].xPos = 10;
+ reducedDeploymentData[0].time = new Date(reducedDeploymentData[0].created_at);
+ describe('Methods', () => {
+ it('refText shows the ref when a tag is available', () => {
+ reducedDeploymentData[0].tag = '1.0';
+ const component = createComponent({
+ showDeployInfo: false,
+ deploymentData: reducedDeploymentData,
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ });
+
+ expect(
+ component.refText(reducedDeploymentData[0]),
+ ).toEqual(reducedDeploymentData[0].ref);
+ });
+
+ it('refText shows the sha when no tag is available', () => {
+ reducedDeploymentData[0].tag = null;
+ const component = createComponent({
+ showDeployInfo: false,
+ deploymentData: reducedDeploymentData,
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ });
+
+ expect(
+ component.refText(reducedDeploymentData[0]),
+ ).toContain('f5bcd1');
+ });
+
+ it('nameDeploymentClass creates a class with the prefix deploy-info-', () => {
+ const component = createComponent({
+ showDeployInfo: false,
+ deploymentData: reducedDeploymentData,
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ });
+
+ expect(
+ component.nameDeploymentClass(reducedDeploymentData[0]),
+ ).toContain('deploy-info');
+ });
+
+ it('transformDeploymentGroup translates an available deployment', () => {
+ const component = createComponent({
+ showDeployInfo: false,
+ deploymentData: reducedDeploymentData,
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ });
+
+ expect(
+ component.transformDeploymentGroup(reducedDeploymentData[0]),
+ ).toContain('translate(11, 20)');
+ });
+
+ it('hides the deployment flag', () => {
+ reducedDeploymentData[0].showDeploymentFlag = false;
+ const component = createComponent({
+ showDeployInfo: true,
+ deploymentData: reducedDeploymentData,
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ });
+
+ expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull();
+ });
+
+ it('shows the deployment flag', () => {
+ reducedDeploymentData[0].showDeploymentFlag = true;
+ const component = createComponent({
+ showDeployInfo: true,
+ deploymentData: reducedDeploymentData,
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ });
+
+ expect(
+ component.$el.querySelector('.js-deploy-info-box').style.display,
+ ).not.toEqual('display: none;');
+ });
+
+ it('shows the refText inside a text element with the deploy-info-text class', () => {
+ reducedDeploymentData[0].showDeploymentFlag = true;
+ const component = createComponent({
+ showDeployInfo: true,
+ deploymentData: reducedDeploymentData,
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ });
+
+ expect(
+ component.$el.querySelector('.deploy-info-text').firstChild.nodeValue.trim(),
+ ).toEqual(component.refText(reducedDeploymentData[0]));
+ });
+
+ it('should contain a hidden gradient', () => {
+ const component = createComponent({
+ showDeployInfo: true,
+ deploymentData: reducedDeploymentData,
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ });
+
+ expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull();
+ });
+
+ describe('Computed props', () => {
+ it('calculatedHeight', () => {
+ const component = createComponent({
+ showDeployInfo: true,
+ deploymentData: reducedDeploymentData,
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ });
+
+ expect(component.calculatedHeight).toEqual(180);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js
new file mode 100644
index 00000000000..14794cbfd50
--- /dev/null
+++ b/spec/javascripts/monitoring/graph/flag_spec.js
@@ -0,0 +1,72 @@
+import Vue from 'vue';
+import GraphFlag from '~/monitoring/components/graph/flag.vue';
+
+const createComponent = (propsData) => {
+ const Component = Vue.extend(GraphFlag);
+
+ return new Component({
+ propsData,
+ }).$mount();
+};
+
+function getCoordinate(component, selector, coordinate) {
+ const coordinateVal = component.$el.querySelector(selector).getAttribute(coordinate);
+ return parseInt(coordinateVal, 10);
+}
+
+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,
+ });
+
+ expect(getCoordinate(component, '.selected-metric-line', 'x1'))
+ .toEqual(component.currentXCoordinate);
+ expect(getCoordinate(component, '.selected-metric-line', 'x2'))
+ .toEqual(component.currentXCoordinate);
+ });
+
+ 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 svg = component.$el.querySelector('.rect-text-metric');
+ expect(svg.tagName).toEqual('svg');
+ expect(parseInt(svg.getAttribute('x'), 10)).toEqual(component.currentFlagPosition);
+ });
+
+ 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,
+ });
+
+ expect(component.calculatedHeight).toEqual(180);
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/graph/legend_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js
new file mode 100644
index 00000000000..da2fbd26e23
--- /dev/null
+++ b/spec/javascripts/monitoring/graph/legend_spec.js
@@ -0,0 +1,107 @@
+import Vue from 'vue';
+import GraphLegend from '~/monitoring/components/graph/legend.vue';
+import measurements from '~/monitoring/utils/measurements';
+import createTimeSeries from '~/monitoring/utils/multiple_time_series';
+import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data';
+
+const createComponent = (propsData) => {
+ const Component = Vue.extend(GraphLegend);
+
+ return new Component({
+ propsData,
+ }).$mount();
+};
+
+const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
+
+const defaultValuesComponent = {
+ graphWidth: 500,
+ graphHeight: 300,
+ graphHeightOffset: 120,
+ margin: measurements.large.margin,
+ measurements: measurements.large,
+ areaColorRgb: '#f0f0f0',
+ legendTitle: 'Title',
+ yAxisLabel: 'Values',
+ metricUsage: 'Value',
+ unitOfDisplay: 'Req/Sec',
+ currentDataIndex: 0,
+};
+
+const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result,
+ defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight,
+ defaultValuesComponent.graphHeightOffset);
+
+defaultValuesComponent.timeSeries = timeSeries;
+
+function getTextFromNode(component, selector) {
+ return component.$el.querySelector(selector).firstChild.nodeValue.trim();
+}
+
+describe('GraphLegend', () => {
+ describe('Computed props', () => {
+ it('textTransform', () => {
+ const component = createComponent(defaultValuesComponent);
+
+ expect(component.textTransform).toContain('translate(15, 120) rotate(-90)');
+ });
+
+ it('xPosition', () => {
+ const component = createComponent(defaultValuesComponent);
+
+ expect(component.xPosition).toEqual(180);
+ });
+
+ it('yPosition', () => {
+ const component = createComponent(defaultValuesComponent);
+
+ expect(component.yPosition).toEqual(240);
+ });
+
+ it('rectTransform', () => {
+ const component = createComponent(defaultValuesComponent);
+
+ expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)');
+ });
+ });
+
+ describe('methods', () => {
+ it('translateLegendGroup should only change Y direction', () => {
+ const component = createComponent(defaultValuesComponent);
+
+ const translatedCoordinate = component.translateLegendGroup(1);
+ expect(translatedCoordinate.indexOf('translate(0, ')).not.toEqual(-1);
+ });
+
+ it('formatMetricUsage should contain the unit of display and the current value selected via "currentDataIndex"', () => {
+ const component = createComponent(defaultValuesComponent);
+
+ const formattedMetricUsage = component.formatMetricUsage(timeSeries[0]);
+ const valueFromSeries = timeSeries[0].values[component.currentDataIndex].value;
+ expect(formattedMetricUsage.indexOf(component.unitOfDisplay)).not.toEqual(-1);
+ expect(formattedMetricUsage.indexOf(valueFromSeries)).not.toEqual(-1);
+ });
+ });
+
+ it('has 2 rect-axis-text rect svg elements', () => {
+ const component = createComponent(defaultValuesComponent);
+
+ expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2);
+ });
+
+ it('contains text to signal the usage, title and time', () => {
+ 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(getTextFromNode(component, '.y-label-text')).toEqual(component.yAxisLabel);
+ });
+
+ it('should contain the same number of legend groups as the timeSeries length', () => {
+ const component = createComponent(defaultValuesComponent);
+
+ expect(component.$el.querySelectorAll('.legend-group').length).toEqual(component.timeSeries.length);
+ });
+});
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
new file mode 100644
index 00000000000..7d8b0744af1
--- /dev/null
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -0,0 +1,89 @@
+import Vue from 'vue';
+import Graph from '~/monitoring/components/graph.vue';
+import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
+import eventHub from '~/monitoring/event_hub';
+import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
+
+const createComponent = (propsData) => {
+ const Component = Vue.extend(Graph);
+
+ return new Component({
+ propsData,
+ }).$mount();
+};
+
+const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
+
+describe('Graph', () => {
+ beforeEach(() => {
+ spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({});
+ });
+
+ it('has a title', () => {
+ const component = createComponent({
+ graphData: convertedMetrics[1],
+ classType: 'col-md-6',
+ updateAspectRatio: false,
+ deploymentData,
+ });
+
+ expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title);
+ });
+
+ describe('Computed props', () => {
+ it('axisTransform translates an element Y position depending of its height', () => {
+ const component = createComponent({
+ graphData: convertedMetrics[1],
+ classType: 'col-md-6',
+ updateAspectRatio: false,
+ deploymentData,
+ });
+
+ const transformedHeight = `${component.graphHeight - 100}`;
+ expect(component.axisTransform.indexOf(transformedHeight))
+ .not.toEqual(-1);
+ });
+
+ it('outterViewBox gets a width and height property based on the DOM size of the element', () => {
+ const component = createComponent({
+ graphData: convertedMetrics[1],
+ classType: 'col-md-6',
+ updateAspectRatio: false,
+ deploymentData,
+ });
+
+ const viewBoxArray = component.outterViewBox.split(' ');
+ expect(typeof component.outterViewBox).toEqual('string');
+ expect(viewBoxArray[2]).toEqual(component.graphWidth.toString());
+ expect(viewBoxArray[3]).toEqual(component.graphHeight.toString());
+ });
+ });
+
+ it('sends an event to the eventhub when it has finished resizing', (done) => {
+ const component = createComponent({
+ graphData: convertedMetrics[1],
+ classType: 'col-md-6',
+ updateAspectRatio: false,
+ deploymentData,
+ });
+ spyOn(eventHub, '$emit');
+
+ component.updateAspectRatio = true;
+ Vue.nextTick(() => {
+ expect(eventHub.$emit).toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('has a title for the y-axis and the chart legend that comes from the backend', () => {
+ const component = createComponent({
+ graphData: convertedMetrics[1],
+ classType: 'col-md-6',
+ updateAspectRatio: false,
+ deploymentData,
+ });
+
+ expect(component.yAxisLabel).toEqual(component.graphData.y_label);
+ expect(component.legendTitle).toEqual(component.graphData.queries[0].label);
+ });
+});
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index b69f4eddffc..3d399f2bb95 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -2473,1754 +2473,5848 @@ export const statePaths = {
documentationPath: '/help/administration/monitoring/prometheus/index.md',
};
-export const singleRowMetrics = [
- {
- 'title': 'CPU usage',
- 'weight': 1,
- 'y_label': 'Memory',
- 'queries': [
- {
- 'query_range': 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100',
- 'label': 'Container CPU',
- 'result': [
- {
- 'metric': {
-
- },
- 'values': [
- {
- 'time': '2017-06-04T21:22:59.508Z',
- 'value': '0.06335544298150002'
- },
- {
- 'time': '2017-06-04T21:23:59.508Z',
- 'value': '0.0420347312480917'
- },
- {
- 'time': '2017-06-04T21:24:59.508Z',
- 'value': '0.0023175131665412706'
- },
- {
- 'time': '2017-06-04T21:25:59.508Z',
- 'value': '0.002315870476190476'
- },
- {
- 'time': '2017-06-04T21:26:59.508Z',
- 'value': '0.0025005961904761894'
- },
- {
- 'time': '2017-06-04T21:27:59.508Z',
- 'value': '0.0024612605834341264'
- },
- {
- 'time': '2017-06-04T21:28:59.508Z',
- 'value': '0.002313129398767631'
- },
- {
- 'time': '2017-06-04T21:29:59.508Z',
- 'value': '0.002411067353663882'
- },
- {
- 'time': '2017-06-04T21:30:59.508Z',
- 'value': '0.002577309263721303'
- },
- {
- 'time': '2017-06-04T21:31:59.508Z',
- 'value': '0.00242688307730403'
- },
- {
- 'time': '2017-06-04T21:32:59.508Z',
- 'value': '0.0024168360301330457'
- },
- {
- 'time': '2017-06-04T21:33:59.508Z',
- 'value': '0.0020449528090743714'
- },
- {
- 'time': '2017-06-04T21:34:59.508Z',
- 'value': '0.0019149619047619036'
- },
- {
- 'time': '2017-06-04T21:35:59.508Z',
- 'value': '0.0024491714364625094'
- },
- {
- 'time': '2017-06-04T21:36:59.508Z',
- 'value': '0.002728773131172677'
- },
- {
- 'time': '2017-06-04T21:37:59.508Z',
- 'value': '0.0028439119047618997'
- },
- {
- 'time': '2017-06-04T21:38:59.508Z',
- 'value': '0.0026307480952380917'
- },
- {
- 'time': '2017-06-04T21:39:59.508Z',
- 'value': '0.0025024842620546446'
- },
- {
- 'time': '2017-06-04T21:40:59.508Z',
- 'value': '0.002300662387260825'
- },
- {
- 'time': '2017-06-04T21:41:59.508Z',
- 'value': '0.002052890924848337'
- },
- {
- 'time': '2017-06-04T21:42:59.508Z',
- 'value': '0.0023711195238095275'
- },
- {
- 'time': '2017-06-04T21:43:59.508Z',
- 'value': '0.002513477619047618'
- },
- {
- 'time': '2017-06-04T21:44:59.508Z',
- 'value': '0.0023489776287844897'
- },
- {
- 'time': '2017-06-04T21:45:59.508Z',
- 'value': '0.002542572310212481'
- },
- {
- 'time': '2017-06-04T21:46:59.508Z',
- 'value': '0.0024579470671707952'
- },
- {
- 'time': '2017-06-04T21:47:59.508Z',
- 'value': '0.0028725150236664403'
- },
- {
- 'time': '2017-06-04T21:48:59.508Z',
- 'value': '0.0024356089105610525'
- },
- {
- 'time': '2017-06-04T21:49:59.508Z',
- 'value': '0.002544015828269929'
- },
- {
- 'time': '2017-06-04T21:50:59.508Z',
- 'value': '0.0029595013380824906'
- },
- {
- 'time': '2017-06-04T21:51:59.508Z',
- 'value': '0.0023084015085858'
- },
- {
- 'time': '2017-06-04T21:52:59.508Z',
- 'value': '0.0021070500000000083'
- },
- {
- 'time': '2017-06-04T21:53:59.508Z',
- 'value': '0.0022950066191106617'
- },
- {
- 'time': '2017-06-04T21:54:59.508Z',
- 'value': '0.002492719454470995'
- },
- {
- 'time': '2017-06-04T21:55:59.508Z',
- 'value': '0.00244312761904762'
- },
- {
- 'time': '2017-06-04T21:56:59.508Z',
- 'value': '0.0023495500000000028'
- },
- {
- 'time': '2017-06-04T21:57:59.508Z',
- 'value': '0.0020597072353070005'
- },
- {
- 'time': '2017-06-04T21:58:59.508Z',
- 'value': '0.0021482352044800866'
- },
- {
- 'time': '2017-06-04T21:59:59.508Z',
- 'value': '0.002333490000000004'
- },
- {
- 'time': '2017-06-04T22:00:59.508Z',
- 'value': '0.0025899442857142815'
- },
- {
- 'time': '2017-06-04T22:01:59.508Z',
- 'value': '0.002430299999999999'
- },
- {
- 'time': '2017-06-04T22:02:59.508Z',
- 'value': '0.0023550328092113476'
- },
- {
- 'time': '2017-06-04T22:03:59.508Z',
- 'value': '0.0026521871636872793'
- },
- {
- 'time': '2017-06-04T22:04:59.508Z',
- 'value': '0.0023080671428571398'
- },
- {
- 'time': '2017-06-04T22:05:59.508Z',
- 'value': '0.0024108401032390896'
- },
- {
- 'time': '2017-06-04T22:06:59.508Z',
- 'value': '0.002433249366678738'
- },
- {
- 'time': '2017-06-04T22:07:59.508Z',
- 'value': '0.0023242202306688682'
- },
- {
- 'time': '2017-06-04T22:08:59.508Z',
- 'value': '0.002388222857142859'
- },
- {
- 'time': '2017-06-04T22:09:59.508Z',
- 'value': '0.002115974914046794'
- },
- {
- 'time': '2017-06-04T22:10:59.508Z',
- 'value': '0.0025090043331269917'
- },
- {
- 'time': '2017-06-04T22:11:59.508Z',
- 'value': '0.002445507057277277'
- },
- {
- 'time': '2017-06-04T22:12:59.508Z',
- 'value': '0.0026348773751130976'
- },
- {
- 'time': '2017-06-04T22:13:59.508Z',
- 'value': '0.0025616258583088104'
- },
- {
- 'time': '2017-06-04T22:14:59.508Z',
- 'value': '0.0021544093415751505'
- },
- {
- 'time': '2017-06-04T22:15:59.508Z',
- 'value': '0.002649394767668881'
- },
- {
- 'time': '2017-06-04T22:16:59.508Z',
- 'value': '0.0024023332666685705'
- },
- {
- 'time': '2017-06-04T22:17:59.508Z',
- 'value': '0.0025444105294235306'
- },
- {
- 'time': '2017-06-04T22:18:59.508Z',
- 'value': '0.0027298872305772806'
- },
- {
- 'time': '2017-06-04T22:19:59.508Z',
- 'value': '0.0022880104956379287'
- },
- {
- 'time': '2017-06-04T22:20:59.508Z',
- 'value': '0.002473246666666661'
- },
- {
- 'time': '2017-06-04T22:21:59.508Z',
- 'value': '0.002259948381935587'
- },
- {
- 'time': '2017-06-04T22:22:59.508Z',
- 'value': '0.0025778470886268835'
- },
- {
- 'time': '2017-06-04T22:23:59.508Z',
- 'value': '0.002246127910852894'
- },
- {
- 'time': '2017-06-04T22:24:59.508Z',
- 'value': '0.0020697466666666758'
- },
- {
- 'time': '2017-06-04T22:25:59.508Z',
- 'value': '0.00225859722473547'
- },
- {
- 'time': '2017-06-04T22:26:59.508Z',
- 'value': '0.0026466728254554814'
- },
- {
- 'time': '2017-06-04T22:27:59.508Z',
- 'value': '0.002151247619047619'
- },
- {
- 'time': '2017-06-04T22:28:59.508Z',
- 'value': '0.002324161444543914'
- },
- {
- 'time': '2017-06-04T22:29:59.508Z',
- 'value': '0.002476474313796452'
- },
- {
- 'time': '2017-06-04T22:30:59.508Z',
- 'value': '0.0023922184232080517'
- },
- {
- 'time': '2017-06-04T22:31:59.508Z',
- 'value': '0.0025094934237468933'
- },
- {
- 'time': '2017-06-04T22:32:59.508Z',
- 'value': '0.0025665311098200883'
- },
- {
- 'time': '2017-06-04T22:33:59.508Z',
- 'value': '0.0024154900681661374'
- },
- {
- 'time': '2017-06-04T22:34:59.508Z',
- 'value': '0.0023267450166192037'
- },
- {
- 'time': '2017-06-04T22:35:59.508Z',
- 'value': '0.002156521904761904'
- },
- {
- 'time': '2017-06-04T22:36:59.508Z',
- 'value': '0.0025474356898637007'
- },
- {
- 'time': '2017-06-04T22:37:59.508Z',
- 'value': '0.0025989409624670233'
- },
- {
- 'time': '2017-06-04T22:38:59.508Z',
- 'value': '0.002348336664762987'
- },
- {
- 'time': '2017-06-04T22:39:59.508Z',
- 'value': '0.002665888246554726'
- },
- {
- 'time': '2017-06-04T22:40:59.508Z',
- 'value': '0.002652684787474174'
- },
- {
- 'time': '2017-06-04T22:41:59.508Z',
- 'value': '0.002472620430865355'
- },
- {
- 'time': '2017-06-04T22:42:59.508Z',
- 'value': '0.0020616469210110247'
- },
- {
- 'time': '2017-06-04T22:43:59.508Z',
- 'value': '0.0022434546372311934'
- },
- {
- 'time': '2017-06-04T22:44:59.508Z',
- 'value': '0.0024469386784827982'
- },
- {
- 'time': '2017-06-04T22:45:59.508Z',
- 'value': '0.0026192823809523787'
- },
- {
- 'time': '2017-06-04T22:46:59.508Z',
- 'value': '0.003451999542852798'
- },
- {
- 'time': '2017-06-04T22:47:59.508Z',
- 'value': '0.0031780314285714288'
- },
- {
- 'time': '2017-06-04T22:48:59.508Z',
- 'value': '0.0024403352380952415'
- },
- {
- 'time': '2017-06-04T22:49:59.508Z',
- 'value': '0.001998824761904764'
- },
- {
- 'time': '2017-06-04T22:50:59.508Z',
- 'value': '0.0023792404761904806'
- },
- {
- 'time': '2017-06-04T22:51:59.508Z',
- 'value': '0.002725906190476185'
- },
- {
- 'time': '2017-06-04T22:52:59.508Z',
- 'value': '0.0020989528671155624'
- },
- {
- 'time': '2017-06-04T22:53:59.508Z',
- 'value': '0.00228808226745016'
- },
- {
- 'time': '2017-06-04T22:54:59.508Z',
- 'value': '0.0019860807413192147'
- },
- {
- 'time': '2017-06-04T22:55:59.508Z',
- 'value': '0.0022698085714285897'
- },
- {
- 'time': '2017-06-04T22:56:59.508Z',
- 'value': '0.0022839098467604415'
- },
- {
- 'time': '2017-06-04T22:57:59.508Z',
- 'value': '0.002531114761904749'
- },
- {
- 'time': '2017-06-04T22:58:59.508Z',
- 'value': '0.0028941072550999016'
- },
- {
- 'time': '2017-06-04T22:59:59.508Z',
- 'value': '0.002547169523809506'
- },
- {
- 'time': '2017-06-04T23:00:59.508Z',
- 'value': '0.0024062999999999958'
- },
- {
- 'time': '2017-06-04T23:01:59.508Z',
- 'value': '0.0026939518471604386'
- },
- {
- 'time': '2017-06-04T23:02:59.508Z',
- 'value': '0.002362901428571429'
- },
- {
- 'time': '2017-06-04T23:03:59.508Z',
- 'value': '0.002663927142857154'
- },
- {
- 'time': '2017-06-04T23:04:59.508Z',
- 'value': '0.0026173314285714354'
- },
- {
- 'time': '2017-06-04T23:05:59.508Z',
- 'value': '0.002326527366406044'
- },
- {
- 'time': '2017-06-04T23:06:59.508Z',
- 'value': '0.002035313809523809'
- },
- {
- 'time': '2017-06-04T23:07:59.508Z',
- 'value': '0.002421447414786533'
- },
- {
- 'time': '2017-06-04T23:08:59.508Z',
- 'value': '0.002898313809523804'
- },
- {
- 'time': '2017-06-04T23:09:59.508Z',
- 'value': '0.002544891856112907'
- },
- {
- 'time': '2017-06-04T23:10:59.508Z',
- 'value': '0.002290625356938882'
- },
- {
- 'time': '2017-06-04T23:11:59.508Z',
- 'value': '0.002483028095238096'
- },
- {
- 'time': '2017-06-04T23:12:59.508Z',
- 'value': '0.0023396832350784237'
- },
- {
- 'time': '2017-06-04T23:13:59.508Z',
- 'value': '0.002085529248176153'
- },
- {
- 'time': '2017-06-04T23:14:59.508Z',
- 'value': '0.0022417815068428012'
- },
- {
- 'time': '2017-06-04T23:15:59.508Z',
- 'value': '0.002660293333333341'
- },
- {
- 'time': '2017-06-04T23:16:59.508Z',
- 'value': '0.0029845149093818226'
- },
- {
- 'time': '2017-06-04T23:17:59.508Z',
- 'value': '0.0027716655079475464'
- },
- {
- 'time': '2017-06-04T23:18:59.508Z',
- 'value': '0.0025217708908741128'
- },
- {
- 'time': '2017-06-04T23:19:59.508Z',
- 'value': '0.0025811235131094055'
- },
- {
- 'time': '2017-06-04T23:20:59.508Z',
- 'value': '0.002209904761904762'
- },
- {
- 'time': '2017-06-04T23:21:59.508Z',
- 'value': '0.0025053322926383344'
- },
- {
- 'time': '2017-06-04T23:22:59.508Z',
- 'value': '0.002350917636526411'
- },
- {
- 'time': '2017-06-04T23:23:59.508Z',
- 'value': '0.0018477500000000078'
- },
- {
- 'time': '2017-06-04T23:24:59.508Z',
- 'value': '0.002427629523809527'
- },
- {
- 'time': '2017-06-04T23:25:59.508Z',
- 'value': '0.0019305498147601655'
- },
- {
- 'time': '2017-06-04T23:26:59.508Z',
- 'value': '0.002097250000000006'
- },
- {
- 'time': '2017-06-04T23:27:59.508Z',
- 'value': '0.002675020952780041'
- },
- {
- 'time': '2017-06-04T23:28:59.508Z',
- 'value': '0.0023142214285714374'
- },
- {
- 'time': '2017-06-04T23:29:59.508Z',
- 'value': '0.0023644723809523737'
- },
- {
- 'time': '2017-06-04T23:30:59.508Z',
- 'value': '0.002108696190476198'
- },
- {
- 'time': '2017-06-04T23:31:59.508Z',
- 'value': '0.0019918289697997194'
- },
- {
- 'time': '2017-06-04T23:32:59.508Z',
- 'value': '0.001583584285714283'
- },
- {
- 'time': '2017-06-04T23:33:59.508Z',
- 'value': '0.002073770226383112'
- },
- {
- 'time': '2017-06-04T23:34:59.508Z',
- 'value': '0.0025877664234966818'
- },
- {
- 'time': '2017-06-04T23:35:59.508Z',
- 'value': '0.0021138238095238147'
- },
- {
- 'time': '2017-06-04T23:36:59.508Z',
- 'value': '0.0022140838095238303'
- },
- {
- 'time': '2017-06-04T23:37:59.508Z',
- 'value': '0.0018592674425248847'
- },
- {
- 'time': '2017-06-04T23:38:59.508Z',
- 'value': '0.0020461969533657016'
- },
- {
- 'time': '2017-06-04T23:39:59.508Z',
- 'value': '0.0021593628571428543'
- },
- {
- 'time': '2017-06-04T23:40:59.508Z',
- 'value': '0.0024330682564928188'
- },
- {
- 'time': '2017-06-04T23:41:59.508Z',
- 'value': '0.0021501804779093174'
- },
- {
- 'time': '2017-06-04T23:42:59.508Z',
- 'value': '0.0025787493928397945'
- },
- {
- 'time': '2017-06-04T23:43:59.508Z',
- 'value': '0.002593657082448396'
- },
- {
- 'time': '2017-06-04T23:44:59.508Z',
- 'value': '0.0021316752380952306'
- },
- {
- 'time': '2017-06-04T23:45:59.508Z',
- 'value': '0.0026972905019952086'
- },
- {
- 'time': '2017-06-04T23:46:59.508Z',
- 'value': '0.002580250764292983'
- },
- {
- 'time': '2017-06-04T23:47:59.508Z',
- 'value': '0.00227103000000001'
- },
- {
- 'time': '2017-06-04T23:48:59.508Z',
- 'value': '0.0023678515647321146'
- },
- {
- 'time': '2017-06-04T23:49:59.508Z',
- 'value': '0.002371472857142866'
- },
- {
- 'time': '2017-06-04T23:50:59.508Z',
- 'value': '0.0026181353688500978'
- },
- {
- 'time': '2017-06-04T23:51:59.508Z',
- 'value': '0.0025609667711121217'
- },
- {
- 'time': '2017-06-04T23:52:59.508Z',
- 'value': '0.0027145308139922557'
- },
- {
- 'time': '2017-06-04T23:53:59.508Z',
- 'value': '0.0024249397613310512'
- },
- {
- 'time': '2017-06-04T23:54:59.508Z',
- 'value': '0.002399907142857147'
- },
- {
- 'time': '2017-06-04T23:55:59.508Z',
- 'value': '0.0024753357142857195'
- },
- {
- 'time': '2017-06-04T23:56:59.508Z',
- 'value': '0.0026179149325231575'
- },
- {
- 'time': '2017-06-04T23:57:59.508Z',
- 'value': '0.0024261340368186956'
- },
- {
- 'time': '2017-06-04T23:58:59.508Z',
- 'value': '0.0021061071428571517'
- },
- {
- 'time': '2017-06-04T23:59:59.508Z',
- 'value': '0.0024033971105037015'
- },
- {
- 'time': '2017-06-05T00:00:59.508Z',
- 'value': '0.0028287676190475956'
- },
- {
- 'time': '2017-06-05T00:01:59.508Z',
- 'value': '0.002499719050294778'
- },
- {
- 'time': '2017-06-05T00:02:59.508Z',
- 'value': '0.0026726102153353856'
- },
- {
- 'time': '2017-06-05T00:03:59.508Z',
- 'value': '0.00262582619047618'
- },
- {
- 'time': '2017-06-05T00:04:59.508Z',
- 'value': '0.002280473147363316'
- },
- {
- 'time': '2017-06-05T00:05:59.508Z',
- 'value': '0.002095581470652675'
- },
- {
- 'time': '2017-06-05T00:06:59.508Z',
- 'value': '0.002270768490828408'
- },
- {
- 'time': '2017-06-05T00:07:59.508Z',
- 'value': '0.002728577415023017'
- },
- {
- 'time': '2017-06-05T00:08:59.508Z',
- 'value': '0.002652512857142863'
- },
- {
- 'time': '2017-06-05T00:09:59.508Z',
- 'value': '0.0022781033924455674'
- },
- {
- 'time': '2017-06-05T00:10:59.508Z',
- 'value': '0.0025345038095238234'
- },
- {
- 'time': '2017-06-05T00:11:59.508Z',
- 'value': '0.002376050020000397'
- },
- {
- 'time': '2017-06-05T00:12:59.508Z',
- 'value': '0.002455068143506122'
- },
- {
- 'time': '2017-06-05T00:13:59.508Z',
- 'value': '0.002826705714285719'
- },
- {
- 'time': '2017-06-05T00:14:59.508Z',
- 'value': '0.002343833692070314'
- },
- {
- 'time': '2017-06-05T00:15:59.508Z',
- 'value': '0.00264853297122164'
- },
- {
- 'time': '2017-06-05T00:16:59.508Z',
- 'value': '0.0027656335117426257'
- },
- {
- 'time': '2017-06-05T00:17:59.508Z',
- 'value': '0.0025896543842439564'
- },
- {
- 'time': '2017-06-05T00:18:59.508Z',
- 'value': '0.002180053237081201'
- },
- {
- 'time': '2017-06-05T00:19:59.508Z',
- 'value': '0.002475245002333342'
- },
- {
- 'time': '2017-06-05T00:20:59.508Z',
- 'value': '0.0027559767805101065'
- },
- {
- 'time': '2017-06-05T00:21:59.508Z',
- 'value': '0.0022294836141296607'
- },
- {
- 'time': '2017-06-05T00:22:59.508Z',
- 'value': '0.0021383590476190643'
- },
- {
- 'time': '2017-06-05T00:23:59.508Z',
- 'value': '0.002085417956361494'
- },
- {
- 'time': '2017-06-05T00:24:59.508Z',
- 'value': '0.0024140319047619013'
- },
- {
- 'time': '2017-06-05T00:25:59.508Z',
- 'value': '0.0024513114285714304'
- },
- {
- 'time': '2017-06-05T00:26:59.508Z',
- 'value': '0.0026932152380952446'
- },
- {
- 'time': '2017-06-05T00:27:59.508Z',
- 'value': '0.0022656844350898517'
- },
- {
- 'time': '2017-06-05T00:28:59.508Z',
- 'value': '0.0024483785714285704'
- },
- {
- 'time': '2017-06-05T00:29:59.508Z',
- 'value': '0.002559505804817207'
- },
- {
- 'time': '2017-06-05T00:30:59.508Z',
- 'value': '0.0019485681088751649'
- },
- {
- 'time': '2017-06-05T00:31:59.508Z',
- 'value': '0.00228367984456996'
- },
- {
- 'time': '2017-06-05T00:32:59.508Z',
- 'value': '0.002522149047619049'
- },
- {
- 'time': '2017-06-05T00:33:59.508Z',
- 'value': '0.0026860117715406737'
- },
- {
- 'time': '2017-06-05T00:34:59.508Z',
- 'value': '0.002679669523809523'
- },
- {
- 'time': '2017-06-05T00:35:59.508Z',
- 'value': '0.0022201920970675937'
- },
- {
- 'time': '2017-06-05T00:36:59.508Z',
- 'value': '0.0022917647619047615'
- },
- {
- 'time': '2017-06-05T00:37:59.508Z',
- 'value': '0.0021774059294673576'
- },
- {
- 'time': '2017-06-05T00:38:59.508Z',
- 'value': '0.0024637766666666763'
- },
- {
- 'time': '2017-06-05T00:39:59.508Z',
- 'value': '0.002470468290174195'
- },
- {
- 'time': '2017-06-05T00:40:59.508Z',
- 'value': '0.0022188616082057812'
- },
- {
- 'time': '2017-06-05T00:41:59.508Z',
- 'value': '0.002421840744373875'
- },
- {
- 'time': '2017-06-05T00:42:59.508Z',
- 'value': '0.0023918266666666547'
- },
- {
- 'time': '2017-06-05T00:43:59.508Z',
- 'value': '0.002195743809523809'
- },
- {
- 'time': '2017-06-05T00:44:59.508Z',
- 'value': '0.0025514828571428687'
- },
- {
- 'time': '2017-06-05T00:45:59.508Z',
- 'value': '0.0027981709349612694'
- },
- {
- 'time': '2017-06-05T00:46:59.508Z',
- 'value': '0.002557977142857146'
- },
- {
- 'time': '2017-06-05T00:47:59.508Z',
- 'value': '0.002213244285714286'
- },
- {
- 'time': '2017-06-05T00:48:59.508Z',
- 'value': '0.0025706738095238046'
- },
- {
- 'time': '2017-06-05T00:49:59.508Z',
- 'value': '0.002210976666666671'
- },
- {
- 'time': '2017-06-05T00:50:59.508Z',
- 'value': '0.002055377091646749'
- },
- {
- 'time': '2017-06-05T00:51:59.508Z',
- 'value': '0.002308368095238119'
- },
- {
- 'time': '2017-06-05T00:52:59.508Z',
- 'value': '0.0024687939885141615'
- },
- {
- 'time': '2017-06-05T00:53:59.508Z',
- 'value': '0.002563018571428578'
- },
- {
- 'time': '2017-06-05T00:54:59.508Z',
- 'value': '0.00240563291078959'
- }
- ]
- }
+export const singleRowMetricsMultipleSeries = [
+ {
+ 'title': 'Multiple Time Series',
+ 'weight': 1,
+ 'y_label': 'Request Rates',
+ 'queries': [
+ {
+ 'query_range': 'sum(rate(nginx_responses_total{environment="production"}[2m])) by (status_code)',
+ 'label': 'Requests',
+ 'unit': 'Req/sec',
+ 'result': [
+ {
+ 'metric': {
+ 'status_code': '1xx'
+ },
+ 'values': [
+ {
+ 'time': '2017-08-27T11:01:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:02:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:03:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:04:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:05:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:06:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:07:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:08:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:09:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:10:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:11:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:12:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:13:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:14:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:15:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:16:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:17:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:18:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:19:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:20:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:21:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:22:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:23:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:24:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:25:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:26:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:27:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:28:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:29:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:30:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:31:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:32:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:33:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:34:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:35:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:36:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:37:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:38:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:39:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:40:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:41:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:42:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:43:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:44:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:45:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:46:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:47:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:48:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:49:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:50:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:51:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:52:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:53:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:54:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:55:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:56:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:57:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:58:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T11:59:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:00:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:01:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:02:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:03:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:04:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:05:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:06:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:07:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:08:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:09:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:10:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:11:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:12:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:13:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:14:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:15:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:16:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:17:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:18:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:19:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:20:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:21:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:22:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:23:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:24:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:25:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:26:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:27:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:28:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:29:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:30:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:31:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:32:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:33:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:34:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:35:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:36:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:37:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:38:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:39:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:40:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:41:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:42:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:43:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:44:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:45:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:46:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:47:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:48:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:49:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:50:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:51:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:52:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:53:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:54:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:55:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:56:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:57:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:58:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T12:59:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:00:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:01:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:02:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:03:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:04:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:05:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:06:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:07:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:08:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:09:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:10:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:11:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:12:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:13:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:14:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:15:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:16:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:17:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:18:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:19:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:20:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:21:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:22:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:23:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:24:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:25:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:26:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:27:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:28:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:29:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:30:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:31:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:32:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:33:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:34:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:35:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:36:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:37:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:38:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:39:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:40:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:41:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:42:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:43:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:44:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:45:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:46:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:47:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:48:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:49:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:50:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:51:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:52:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:53:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:54:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:55:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:56:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:57:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:58:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T13:59:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:00:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:01:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:02:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:03:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:04:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:05:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:06:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:07:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:08:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:09:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:10:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:11:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:12:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:13:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:14:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:15:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:16:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:17:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:18:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:19:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:20:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:21:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:22:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:23:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:24:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:25:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:26:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:27:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:28:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:29:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:30:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:31:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:32:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:33:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:34:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:35:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:36:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:37:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:38:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:39:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:40:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:41:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:42:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:43:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:44:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:45:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:46:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:47:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:48:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:49:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:50:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:51:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:52:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:53:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:54:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:55:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:56:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:57:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:58:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T14:59:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:00:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:01:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:02:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:03:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:04:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:05:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:06:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:07:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:08:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:09:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:10:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:11:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:12:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:13:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:14:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:15:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:16:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:17:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:18:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:19:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:20:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:21:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:22:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:23:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:24:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:25:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:26:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:27:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:28:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:29:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:30:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:31:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:32:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:33:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:34:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:35:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:36:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:37:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:38:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:39:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:40:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:41:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:42:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:43:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:44:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:45:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:46:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:47:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:48:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:49:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:50:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:51:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:52:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:53:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:54:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:55:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:56:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:57:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:58:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T15:59:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:00:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:01:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:02:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:03:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:04:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:05:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:06:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:07:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:08:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:09:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:10:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:11:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:12:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:13:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:14:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:15:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:16:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:17:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:18:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:19:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:20:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:21:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:22:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:23:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:24:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:25:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:26:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:27:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:28:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:29:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:30:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:31:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:32:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:33:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:34:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:35:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:36:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:37:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:38:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:39:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:40:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:41:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:42:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:43:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:44:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:45:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:46:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:47:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:48:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:49:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:50:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:51:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:52:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:53:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:54:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:55:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:56:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:57:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:58:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T16:59:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:00:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:01:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:02:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:03:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:04:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:05:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:06:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:07:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:08:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:09:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:10:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:11:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:12:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:13:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:14:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:15:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:16:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:17:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:18:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:19:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:20:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:21:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:22:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:23:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:24:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:25:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:26:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:27:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:28:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:29:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:30:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:31:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:32:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:33:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:34:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:35:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:36:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:37:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:38:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:39:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:40:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:41:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:42:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:43:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:44:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:45:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:46:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:47:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:48:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:49:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:50:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:51:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:52:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:53:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:54:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:55:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:56:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:57:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:58:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T17:59:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:00:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:01:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:02:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:03:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:04:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:05:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:06:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:07:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:08:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:09:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:10:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:11:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:12:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:13:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:14:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:15:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:16:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:17:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:18:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:19:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:20:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:21:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:22:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:23:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:24:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:25:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:26:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:27:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:28:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:29:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:30:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:31:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:32:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:33:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:34:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:35:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:36:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:37:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:38:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:39:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:40:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:41:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:42:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:43:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:44:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:45:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:46:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:47:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:48:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:49:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:50:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:51:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:52:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:53:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:54:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:55:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:56:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:57:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:58:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T18:59:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T19:00:51.462Z',
+ 'value': '0'
+ },
+ {
+ 'time': '2017-08-27T19:01:51.462Z',
+ 'value': '0'
+ }
+ ]
+ },
+ {
+ 'metric': {
+ 'status_code': '2xx'
+ },
+ 'values': [
+ {
+ 'time': '2017-08-27T11:01:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:02:51.462Z',
+ 'value': '1.2571428571428571'
+ },
+ {
+ 'time': '2017-08-27T11:03:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:04:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:05:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:06:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:07:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:08:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:09:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:10:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:11:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:12:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:13:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:14:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:15:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:16:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:17:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:18:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:19:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:20:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:21:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:22:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:23:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:24:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:25:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:26:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:27:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:28:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:29:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:30:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:31:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:32:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:33:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:34:51.462Z',
+ 'value': '1.333320635041571'
+ },
+ {
+ 'time': '2017-08-27T11:35:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:36:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:37:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:38:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:39:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:40:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:41:51.462Z',
+ 'value': '1.3333587306424883'
+ },
+ {
+ 'time': '2017-08-27T11:42:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:43:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:44:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:45:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:46:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:47:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:48:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:49:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T11:50:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:51:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:52:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:53:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:54:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:55:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:56:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:57:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T11:58:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T11:59:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:00:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:01:51.462Z',
+ 'value': '1.3333460318669703'
+ },
+ {
+ 'time': '2017-08-27T12:02:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:03:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:04:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:05:51.462Z',
+ 'value': '1.31427319739812'
+ },
+ {
+ 'time': '2017-08-27T12:06:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:07:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:08:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:09:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:10:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:11:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:12:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:13:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:14:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:15:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:16:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:17:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:18:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:19:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:20:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:21:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:22:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:23:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:24:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:25:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:26:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:27:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:28:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:29:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:30:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:31:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:32:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:33:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:34:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:35:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:36:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:37:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:38:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:39:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:40:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:41:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:42:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:43:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:44:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:45:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:46:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:47:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:48:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:49:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:50:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:51:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:52:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:53:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:54:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T12:55:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:56:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:57:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T12:58:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T12:59:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T13:00:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T13:01:51.462Z',
+ 'value': '1.295225759754669'
+ },
+ {
+ 'time': '2017-08-27T13:02:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:03:51.462Z',
+ 'value': '1.2952627669098458'
+ },
+ {
+ 'time': '2017-08-27T13:04:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:05:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:06:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:07:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:08:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:09:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:10:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:11:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:12:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:13:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:14:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:15:51.462Z',
+ 'value': '1.2571428571428571'
+ },
+ {
+ 'time': '2017-08-27T13:16:51.462Z',
+ 'value': '1.3333587306424883'
+ },
+ {
+ 'time': '2017-08-27T13:17:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:18:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:19:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:20:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T13:21:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:22:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:23:51.462Z',
+ 'value': '1.276190476190476'
+ },
+ {
+ 'time': '2017-08-27T13:24:51.462Z',
+ 'value': '1.2571428571428571'
+ },
+ {
+ 'time': '2017-08-27T13:25:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T13:26:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:27:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T13:28:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:29:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:30:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:31:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:32:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T13:33:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:34:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:35:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T13:36:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:37:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:38:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:39:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:40:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:41:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:42:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:43:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:44:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:45:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:46:51.462Z',
+ 'value': '1.2571428571428571'
+ },
+ {
+ 'time': '2017-08-27T13:47:51.462Z',
+ 'value': '1.276190476190476'
+ },
+ {
+ 'time': '2017-08-27T13:48:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T13:49:51.462Z',
+ 'value': '1.295225759754669'
+ },
+ {
+ 'time': '2017-08-27T13:50:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:51:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:52:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:53:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:54:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:55:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:56:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T13:57:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T13:58:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T13:59:51.462Z',
+ 'value': '1.295225759754669'
+ },
+ {
+ 'time': '2017-08-27T14:00:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T14:01:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T14:02:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:03:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:04:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:05:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T14:06:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:07:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:08:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:09:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:10:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:11:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:12:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:13:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:14:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:15:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:16:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:17:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T14:18:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:19:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T14:20:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:21:51.462Z',
+ 'value': '1.3333079369916765'
+ },
+ {
+ 'time': '2017-08-27T14:22:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:23:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:24:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:25:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T14:26:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:27:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T14:28:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:29:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:30:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T14:31:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:32:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:33:51.462Z',
+ 'value': '1.2571428571428571'
+ },
+ {
+ 'time': '2017-08-27T14:34:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:35:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:36:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:37:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:38:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T14:39:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:40:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:41:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:42:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:43:51.462Z',
+ 'value': '1.276190476190476'
+ },
+ {
+ 'time': '2017-08-27T14:44:51.462Z',
+ 'value': '1.2571428571428571'
+ },
+ {
+ 'time': '2017-08-27T14:45:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:46:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:47:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:48:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:49:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:50:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:51:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T14:52:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:53:51.462Z',
+ 'value': '1.333320635041571'
+ },
+ {
+ 'time': '2017-08-27T14:54:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:55:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T14:56:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:57:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T14:58:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T14:59:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T15:00:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:01:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:02:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:03:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:04:51.462Z',
+ 'value': '1.2571428571428571'
+ },
+ {
+ 'time': '2017-08-27T15:05:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:06:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:07:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:08:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:09:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:10:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:11:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:12:51.462Z',
+ 'value': '1.31427319739812'
+ },
+ {
+ 'time': '2017-08-27T15:13:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:14:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:15:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:16:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T15:17:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:18:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:19:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:20:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T15:21:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:22:51.462Z',
+ 'value': '1.3333460318669703'
+ },
+ {
+ 'time': '2017-08-27T15:23:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:24:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:25:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:26:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:27:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:28:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:29:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:30:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:31:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T15:32:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:33:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T15:34:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:35:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T15:36:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:37:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:38:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T15:39:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:40:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:41:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:42:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:43:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:44:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:45:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:46:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:47:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:48:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:49:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T15:50:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:51:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:52:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:53:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:54:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:55:51.462Z',
+ 'value': '1.3333587306424883'
+ },
+ {
+ 'time': '2017-08-27T15:56:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T15:57:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:58:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T15:59:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:00:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:01:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:02:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:03:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:04:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:05:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:06:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:07:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:08:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:09:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:10:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:11:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:12:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:13:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:14:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:15:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:16:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:17:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:18:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:19:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:20:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:21:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:22:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:23:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:24:51.462Z',
+ 'value': '1.295225759754669'
+ },
+ {
+ 'time': '2017-08-27T16:25:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:26:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:27:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:28:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:29:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:30:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:31:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:32:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:33:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:34:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:35:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:36:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:37:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:38:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:39:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:40:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:41:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:42:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:43:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:44:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:45:51.462Z',
+ 'value': '1.3142982314117277'
+ },
+ {
+ 'time': '2017-08-27T16:46:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:47:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:48:51.462Z',
+ 'value': '1.333320635041571'
+ },
+ {
+ 'time': '2017-08-27T16:49:51.462Z',
+ 'value': '1.31427319739812'
+ },
+ {
+ 'time': '2017-08-27T16:50:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:51:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:52:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:53:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:54:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:55:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T16:56:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:57:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T16:58:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T16:59:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:00:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:01:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:02:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:03:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:04:51.462Z',
+ 'value': '1.2952504309564854'
+ },
+ {
+ 'time': '2017-08-27T17:05:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T17:06:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:07:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T17:08:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T17:09:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:10:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:11:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:12:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:13:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:14:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:15:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:16:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:17:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:18:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:19:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:20:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:21:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:22:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:23:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:24:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T17:25:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:26:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:27:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:28:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:29:51.462Z',
+ 'value': '1.295225759754669'
+ },
+ {
+ 'time': '2017-08-27T17:30:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:31:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:32:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:33:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:34:51.462Z',
+ 'value': '1.295225759754669'
+ },
+ {
+ 'time': '2017-08-27T17:35:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:36:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T17:37:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:38:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:39:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:40:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:41:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:42:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:43:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:44:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T17:45:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:46:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:47:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:48:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T17:49:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:50:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T17:51:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:52:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:53:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:54:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:55:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T17:56:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:57:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T17:58:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T17:59:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T18:00:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:01:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:02:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:03:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:04:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:05:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:06:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:07:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:08:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:09:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:10:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:11:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:12:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T18:13:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:14:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:15:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:16:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:17:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:18:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:19:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:20:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:21:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:22:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:23:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:24:51.462Z',
+ 'value': '1.2571428571428571'
+ },
+ {
+ 'time': '2017-08-27T18:25:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:26:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:27:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:28:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:29:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:30:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:31:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:32:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:33:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:34:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:35:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:36:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:37:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T18:38:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:39:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:40:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:41:51.462Z',
+ 'value': '1.580952380952381'
+ },
+ {
+ 'time': '2017-08-27T18:42:51.462Z',
+ 'value': '1.7333333333333334'
+ },
+ {
+ 'time': '2017-08-27T18:43:51.462Z',
+ 'value': '2.057142857142857'
+ },
+ {
+ 'time': '2017-08-27T18:44:51.462Z',
+ 'value': '2.1904761904761902'
+ },
+ {
+ 'time': '2017-08-27T18:45:51.462Z',
+ 'value': '1.8285714285714287'
+ },
+ {
+ 'time': '2017-08-27T18:46:51.462Z',
+ 'value': '2.1142857142857143'
+ },
+ {
+ 'time': '2017-08-27T18:47:51.462Z',
+ 'value': '1.619047619047619'
+ },
+ {
+ 'time': '2017-08-27T18:48:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:49:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:50:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T18:51:51.462Z',
+ 'value': '1.2952504309564854'
+ },
+ {
+ 'time': '2017-08-27T18:52:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:53:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:54:51.462Z',
+ 'value': '1.3333333333333333'
+ },
+ {
+ 'time': '2017-08-27T18:55:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:56:51.462Z',
+ 'value': '1.314285714285714'
+ },
+ {
+ 'time': '2017-08-27T18:57:51.462Z',
+ 'value': '1.295238095238095'
+ },
+ {
+ 'time': '2017-08-27T18:58:51.462Z',
+ 'value': '1.7142857142857142'
+ },
+ {
+ 'time': '2017-08-27T18:59:51.462Z',
+ 'value': '1.7333333333333334'
+ },
+ {
+ 'time': '2017-08-27T19:00:51.462Z',
+ 'value': '1.3904761904761904'
+ },
+ {
+ 'time': '2017-08-27T19:01:51.462Z',
+ 'value': '1.5047619047619047'
+ }
+ ]
+ },
+ ]
+ }
]
- }
- ]
- },
- {
- 'title': 'Memory usage',
- 'weight': 1,
- 'y_label': 'Values',
- 'queries': [
- {
- 'query_range': 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20',
- 'label': 'Container memory',
- 'unit': 'MiB',
- 'result': [
- {
- 'metric': {
+ },
+ {
+ 'title': 'Throughput',
+ 'weight': 1,
+ 'y_label': 'Requests / Sec',
+ 'queries': [
+ {
+ 'query_range': 'sum(rate(nginx_requests_total{server_zone!=\'*\', server_zone!=\'_\', container_name!=\'POD\',environment=\'production\'}[2m]))',
+ 'label': 'Total',
+ 'unit': 'req / sec',
+ 'result': [
+ {
+ 'metric': {
- },
- 'values': [
- {
- 'time': '2017-06-04T21:22:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:23:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:24:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:25:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:26:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:27:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:28:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:29:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:30:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:31:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:32:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:33:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:34:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:35:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:36:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:37:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:38:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:39:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:40:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:41:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:42:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:43:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:44:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:45:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:46:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:47:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:48:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:49:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:50:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:51:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:52:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:53:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:54:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:55:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:56:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:57:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:58:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T21:59:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:00:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:01:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:02:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:03:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:04:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:05:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:06:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:07:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:08:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:09:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:10:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:11:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:12:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:13:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:14:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:15:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:16:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:17:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:18:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:19:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:20:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:21:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:22:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:23:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:24:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:25:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:26:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:27:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:28:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:29:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:30:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:31:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:32:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:33:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:34:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:35:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:36:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:37:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:38:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:39:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:40:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:41:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:42:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:43:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:44:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:45:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:46:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:47:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:48:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:49:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:50:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:51:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:52:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:53:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:54:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:55:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:56:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:57:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:58:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T22:59:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:00:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:01:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:02:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:03:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:04:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:05:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:06:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:07:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:08:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:09:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:10:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:11:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:12:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:13:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:14:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:15:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:16:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:17:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:18:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:19:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:20:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:21:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:22:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:23:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:24:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:25:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:26:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:27:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:28:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:29:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:30:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:31:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:32:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:33:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:34:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:35:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:36:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:37:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:38:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:39:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:40:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:41:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:42:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:43:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:44:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:45:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:46:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:47:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:48:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:49:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:50:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:51:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:52:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:53:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:54:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:55:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:56:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:57:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:58:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-04T23:59:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:00:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:01:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:02:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:03:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:04:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:05:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:06:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:07:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:08:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:09:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:10:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:11:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:12:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:13:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:14:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:15:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:16:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:17:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:18:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:19:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:20:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:21:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:22:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:23:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:24:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:25:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:26:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:27:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:28:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:29:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:30:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:31:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:32:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:33:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:34:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:35:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:36:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:37:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:38:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:39:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:40:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:41:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:42:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:43:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:44:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:45:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:46:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:47:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:48:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:49:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:50:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:51:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:52:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:53:59.508Z',
- 'value': '15.0859375'
- },
- {
- 'time': '2017-06-05T00:54:59.508Z',
- 'value': '15.0859375'
- }
- ]
- }
+ },
+ 'values': [
+ {
+ 'time': '2017-08-27T11:01:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:02:51.462Z',
+ 'value': '0.45714285714285713'
+ },
+ {
+ 'time': '2017-08-27T11:03:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:04:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:05:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:06:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:07:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:08:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:09:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:10:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:11:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:12:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:13:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:14:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:15:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:16:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:17:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:18:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:19:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:20:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:21:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:22:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:23:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:24:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:25:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:26:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:27:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:28:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:29:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:30:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:31:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:32:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:33:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:34:51.462Z',
+ 'value': '0.4952333787297264'
+ },
+ {
+ 'time': '2017-08-27T11:35:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:36:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:37:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:38:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:39:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:40:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:41:51.462Z',
+ 'value': '0.49524752852435283'
+ },
+ {
+ 'time': '2017-08-27T11:42:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:43:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:44:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:45:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:46:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:47:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:48:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:49:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T11:50:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:51:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:52:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:53:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:54:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:55:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:56:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:57:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T11:58:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T11:59:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:00:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:01:51.462Z',
+ 'value': '0.49524281183630325'
+ },
+ {
+ 'time': '2017-08-27T12:02:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:03:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:04:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:05:51.462Z',
+ 'value': '0.4857096599080009'
+ },
+ {
+ 'time': '2017-08-27T12:06:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:07:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:08:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:09:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:10:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:11:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:12:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:13:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:14:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:15:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:16:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:17:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:18:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:19:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:20:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:21:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:22:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:23:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:24:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:25:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:26:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:27:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:28:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:29:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:30:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:31:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:32:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:33:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:34:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:35:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:36:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:37:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:38:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:39:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:40:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:41:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:42:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:43:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:44:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:45:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:46:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:47:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:48:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:49:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:50:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:51:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:52:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:53:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:54:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T12:55:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:56:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:57:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T12:58:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T12:59:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T13:00:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T13:01:51.462Z',
+ 'value': '0.4761859410862754'
+ },
+ {
+ 'time': '2017-08-27T13:02:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:03:51.462Z',
+ 'value': '0.4761995466580315'
+ },
+ {
+ 'time': '2017-08-27T13:04:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:05:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:06:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:07:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:08:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:09:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:10:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:11:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:12:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:13:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:14:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:15:51.462Z',
+ 'value': '0.45714285714285713'
+ },
+ {
+ 'time': '2017-08-27T13:16:51.462Z',
+ 'value': '0.49524752852435283'
+ },
+ {
+ 'time': '2017-08-27T13:17:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:18:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:19:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:20:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T13:21:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:22:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:23:51.462Z',
+ 'value': '0.4666666666666667'
+ },
+ {
+ 'time': '2017-08-27T13:24:51.462Z',
+ 'value': '0.45714285714285713'
+ },
+ {
+ 'time': '2017-08-27T13:25:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T13:26:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:27:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T13:28:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:29:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:30:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:31:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:32:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T13:33:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:34:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:35:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T13:36:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:37:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:38:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:39:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:40:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:41:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:42:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:43:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:44:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:45:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:46:51.462Z',
+ 'value': '0.45714285714285713'
+ },
+ {
+ 'time': '2017-08-27T13:47:51.462Z',
+ 'value': '0.4666666666666667'
+ },
+ {
+ 'time': '2017-08-27T13:48:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T13:49:51.462Z',
+ 'value': '0.4761859410862754'
+ },
+ {
+ 'time': '2017-08-27T13:50:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:51:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:52:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:53:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:54:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:55:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:56:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T13:57:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T13:58:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T13:59:51.462Z',
+ 'value': '0.4761859410862754'
+ },
+ {
+ 'time': '2017-08-27T14:00:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T14:01:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T14:02:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:03:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:04:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:05:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T14:06:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:07:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:08:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:09:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:10:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:11:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:12:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:13:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:14:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:15:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:16:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:17:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T14:18:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:19:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T14:20:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:21:51.462Z',
+ 'value': '0.4952286623111941'
+ },
+ {
+ 'time': '2017-08-27T14:22:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:23:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:24:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:25:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T14:26:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:27:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T14:28:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:29:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:30:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T14:31:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:32:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:33:51.462Z',
+ 'value': '0.45714285714285713'
+ },
+ {
+ 'time': '2017-08-27T14:34:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:35:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:36:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:37:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:38:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T14:39:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:40:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:41:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:42:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:43:51.462Z',
+ 'value': '0.4666666666666667'
+ },
+ {
+ 'time': '2017-08-27T14:44:51.462Z',
+ 'value': '0.45714285714285713'
+ },
+ {
+ 'time': '2017-08-27T14:45:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:46:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:47:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:48:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:49:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:50:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:51:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T14:52:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:53:51.462Z',
+ 'value': '0.4952333787297264'
+ },
+ {
+ 'time': '2017-08-27T14:54:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:55:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T14:56:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:57:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T14:58:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T14:59:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T15:00:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:01:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:02:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:03:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:04:51.462Z',
+ 'value': '0.45714285714285713'
+ },
+ {
+ 'time': '2017-08-27T15:05:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:06:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:07:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:08:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:09:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:10:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:11:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:12:51.462Z',
+ 'value': '0.4857096599080009'
+ },
+ {
+ 'time': '2017-08-27T15:13:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:14:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:15:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:16:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T15:17:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:18:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:19:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:20:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T15:21:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:22:51.462Z',
+ 'value': '0.49524281183630325'
+ },
+ {
+ 'time': '2017-08-27T15:23:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:24:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:25:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:26:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:27:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:28:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:29:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:30:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:31:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T15:32:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:33:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T15:34:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:35:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T15:36:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:37:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:38:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T15:39:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:40:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:41:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:42:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:43:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:44:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:45:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:46:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:47:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:48:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:49:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T15:50:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:51:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:52:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:53:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:54:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:55:51.462Z',
+ 'value': '0.49524752852435283'
+ },
+ {
+ 'time': '2017-08-27T15:56:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T15:57:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:58:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T15:59:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:00:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:01:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:02:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:03:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:04:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:05:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:06:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:07:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:08:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:09:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:10:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:11:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:12:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:13:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:14:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:15:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:16:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:17:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:18:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:19:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:20:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:21:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:22:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:23:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:24:51.462Z',
+ 'value': '0.4761859410862754'
+ },
+ {
+ 'time': '2017-08-27T16:25:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:26:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:27:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:28:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:29:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:30:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:31:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:32:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:33:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:34:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:35:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:36:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:37:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:38:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:39:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:40:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:41:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:42:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:43:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:44:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:45:51.462Z',
+ 'value': '0.485718911608682'
+ },
+ {
+ 'time': '2017-08-27T16:46:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:47:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:48:51.462Z',
+ 'value': '0.4952333787297264'
+ },
+ {
+ 'time': '2017-08-27T16:49:51.462Z',
+ 'value': '0.4857096599080009'
+ },
+ {
+ 'time': '2017-08-27T16:50:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:51:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:52:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:53:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:54:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:55:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T16:56:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:57:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T16:58:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T16:59:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:00:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:01:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:02:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:03:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:04:51.462Z',
+ 'value': '0.47619501138106085'
+ },
+ {
+ 'time': '2017-08-27T17:05:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T17:06:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:07:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T17:08:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T17:09:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:10:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:11:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:12:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:13:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:14:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:15:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:16:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:17:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:18:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:19:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:20:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:21:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:22:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:23:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:24:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T17:25:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:26:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:27:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:28:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:29:51.462Z',
+ 'value': '0.4761859410862754'
+ },
+ {
+ 'time': '2017-08-27T17:30:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:31:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:32:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:33:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:34:51.462Z',
+ 'value': '0.4761859410862754'
+ },
+ {
+ 'time': '2017-08-27T17:35:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:36:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T17:37:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:38:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:39:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:40:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:41:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:42:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:43:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:44:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T17:45:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:46:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:47:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:48:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T17:49:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:50:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T17:51:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:52:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:53:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:54:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:55:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T17:56:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:57:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T17:58:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T17:59:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T18:00:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:01:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:02:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:03:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:04:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:05:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:06:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:07:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:08:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:09:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:10:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:11:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:12:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T18:13:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:14:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:15:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:16:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:17:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:18:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:19:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:20:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:21:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:22:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:23:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:24:51.462Z',
+ 'value': '0.45714285714285713'
+ },
+ {
+ 'time': '2017-08-27T18:25:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:26:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:27:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:28:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:29:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:30:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:31:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:32:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:33:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:34:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:35:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:36:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:37:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T18:38:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:39:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:40:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:41:51.462Z',
+ 'value': '0.6190476190476191'
+ },
+ {
+ 'time': '2017-08-27T18:42:51.462Z',
+ 'value': '0.6952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:43:51.462Z',
+ 'value': '0.857142857142857'
+ },
+ {
+ 'time': '2017-08-27T18:44:51.462Z',
+ 'value': '0.9238095238095239'
+ },
+ {
+ 'time': '2017-08-27T18:45:51.462Z',
+ 'value': '0.7428571428571429'
+ },
+ {
+ 'time': '2017-08-27T18:46:51.462Z',
+ 'value': '0.8857142857142857'
+ },
+ {
+ 'time': '2017-08-27T18:47:51.462Z',
+ 'value': '0.638095238095238'
+ },
+ {
+ 'time': '2017-08-27T18:48:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:49:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:50:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T18:51:51.462Z',
+ 'value': '0.47619501138106085'
+ },
+ {
+ 'time': '2017-08-27T18:52:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:53:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:54:51.462Z',
+ 'value': '0.4952380952380952'
+ },
+ {
+ 'time': '2017-08-27T18:55:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:56:51.462Z',
+ 'value': '0.4857142857142857'
+ },
+ {
+ 'time': '2017-08-27T18:57:51.462Z',
+ 'value': '0.47619047619047616'
+ },
+ {
+ 'time': '2017-08-27T18:58:51.462Z',
+ 'value': '0.6857142857142856'
+ },
+ {
+ 'time': '2017-08-27T18:59:51.462Z',
+ 'value': '0.6952380952380952'
+ },
+ {
+ 'time': '2017-08-27T19:00:51.462Z',
+ 'value': '0.5238095238095237'
+ },
+ {
+ 'time': '2017-08-27T19:01:51.462Z',
+ 'value': '0.5904761904761905'
+ }
+ ]
+ }
+ ]
+ }
]
- }
- ]
- }
+ }
];
+export function convertDatesMultipleSeries(multipleSeries) {
+ const convertedMultiple = multipleSeries;
+ multipleSeries.forEach((column, index) => {
+ let convertedResult = [];
+ convertedResult = column.queries[0].result.map((resultObj) => {
+ const convertedMetrics = {};
+ convertedMetrics.values = resultObj.values.map(val => ({
+ time: new Date(val.time),
+ value: val.value,
+ }));
+ convertedMetrics.metric = resultObj.metric;
+ return convertedMetrics;
+ });
+ convertedMultiple[index].queries[0].result = convertedResult;
+ });
+ return convertedMultiple;
+}
+
export function MonitorMockInterceptor(request, next) {
const body = responseMockData[request.method.toUpperCase()][request.url];
diff --git a/spec/javascripts/monitoring/monitoring_column_spec.js b/spec/javascripts/monitoring/monitoring_column_spec.js
deleted file mode 100644
index c423024dce0..00000000000
--- a/spec/javascripts/monitoring/monitoring_column_spec.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import Vue from 'vue';
-import _ from 'underscore';
-import MonitoringColumn from '~/monitoring/components/monitoring_column.vue';
-import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
-import eventHub from '~/monitoring/event_hub';
-import { deploymentData, singleRowMetrics } from './mock_data';
-
-const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringColumn);
-
- return new Component({
- propsData,
- }).$mount();
-};
-
-describe('MonitoringColumn', () => {
- beforeEach(() => {
- spyOn(MonitoringMixins.methods, 'formatDeployments').and.callFake(function fakeFormat() {
- return {};
- });
- });
-
- it('has a title', () => {
- const component = createComponent({
- columnData: singleRowMetrics[0],
- classType: 'col-md-6',
- updateAspectRatio: false,
- deploymentData,
- });
-
- expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.columnData.title);
- });
-
- it('creates a path for the line and area of the graph', (done) => {
- const component = createComponent({
- columnData: singleRowMetrics[0],
- classType: 'col-md-6',
- updateAspectRatio: false,
- deploymentData,
- });
-
- Vue.nextTick(() => {
- expect(component.area).toBeDefined();
- expect(component.line).toBeDefined();
- expect(typeof component.area).toEqual('string');
- expect(typeof component.line).toEqual('string');
- expect(_.isFunction(component.xScale)).toBe(true);
- expect(_.isFunction(component.yScale)).toBe(true);
- done();
- });
- });
-
- describe('Computed props', () => {
- it('axisTransform translates an element Y position depending of its height', () => {
- const component = createComponent({
- columnData: singleRowMetrics[0],
- classType: 'col-md-6',
- updateAspectRatio: false,
- deploymentData,
- });
-
- const transformedHeight = `${component.graphHeight - 100}`;
- expect(component.axisTransform.indexOf(transformedHeight))
- .not.toEqual(-1);
- });
-
- it('outterViewBox gets a width and height property based on the DOM size of the element', () => {
- const component = createComponent({
- columnData: singleRowMetrics[0],
- classType: 'col-md-6',
- updateAspectRatio: false,
- deploymentData,
- });
-
- const viewBoxArray = component.outterViewBox.split(' ');
- expect(typeof component.outterViewBox).toEqual('string');
- expect(viewBoxArray[2]).toEqual(component.graphWidth.toString());
- expect(viewBoxArray[3]).toEqual(component.graphHeight.toString());
- });
- });
-
- it('sends an event to the eventhub when it has finished resizing', (done) => {
- const component = createComponent({
- columnData: singleRowMetrics[0],
- classType: 'col-md-6',
- updateAspectRatio: false,
- deploymentData,
- });
- spyOn(eventHub, '$emit');
-
- component.updateAspectRatio = true;
- Vue.nextTick(() => {
- expect(eventHub.$emit).toHaveBeenCalled();
- done();
- });
- });
-
- it('has a title for the y-axis and the chart legend that comes from the backend', () => {
- const component = createComponent({
- columnData: singleRowMetrics[0],
- classType: 'col-md-6',
- updateAspectRatio: false,
- deploymentData,
- });
-
- expect(component.yAxisLabel).toEqual(component.columnData.y_label);
- expect(component.legendTitle).toEqual(component.columnData.queries[0].label);
- });
-});
diff --git a/spec/javascripts/monitoring/monitoring_deployment_spec.js b/spec/javascripts/monitoring/monitoring_deployment_spec.js
deleted file mode 100644
index 5cc5b514824..00000000000
--- a/spec/javascripts/monitoring/monitoring_deployment_spec.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import Vue from 'vue';
-import MonitoringState from '~/monitoring/components/monitoring_deployment.vue';
-import { deploymentData } from './mock_data';
-
-const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringState);
-
- return new Component({
- propsData,
- }).$mount();
-};
-
-describe('MonitoringDeployment', () => {
- const reducedDeploymentData = [deploymentData[0]];
- reducedDeploymentData[0].ref = reducedDeploymentData[0].ref.name;
- reducedDeploymentData[0].xPos = 10;
- reducedDeploymentData[0].time = new Date(reducedDeploymentData[0].created_at);
- describe('Methods', () => {
- it('refText shows the ref when a tag is available', () => {
- reducedDeploymentData[0].tag = '1.0';
- const component = createComponent({
- showDeployInfo: false,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphHeightOffset: 120,
- });
-
- expect(
- component.refText(reducedDeploymentData[0]),
- ).toEqual(reducedDeploymentData[0].ref);
- });
-
- it('refText shows the sha when no tag is available', () => {
- reducedDeploymentData[0].tag = null;
- const component = createComponent({
- showDeployInfo: false,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphHeightOffset: 120,
- });
-
- expect(
- component.refText(reducedDeploymentData[0]),
- ).toContain('f5bcd1');
- });
-
- it('nameDeploymentClass creates a class with the prefix deploy-info-', () => {
- const component = createComponent({
- showDeployInfo: false,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphHeightOffset: 120,
- });
-
- expect(
- component.nameDeploymentClass(reducedDeploymentData[0]),
- ).toContain('deploy-info');
- });
-
- it('transformDeploymentGroup translates an available deployment', () => {
- const component = createComponent({
- showDeployInfo: false,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphHeightOffset: 120,
- });
-
- expect(
- component.transformDeploymentGroup(reducedDeploymentData[0]),
- ).toContain('translate(11, 20)');
- });
-
- it('hides the deployment flag', () => {
- reducedDeploymentData[0].showDeploymentFlag = false;
- const component = createComponent({
- showDeployInfo: true,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphHeightOffset: 120,
- });
-
- expect(component.$el.querySelector('.js-deploy-info-box')).toBeNull();
- });
-
- it('shows the deployment flag', () => {
- reducedDeploymentData[0].showDeploymentFlag = true;
- const component = createComponent({
- showDeployInfo: true,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphHeightOffset: 120,
- });
-
- expect(
- component.$el.querySelector('.js-deploy-info-box').style.display,
- ).not.toEqual('display: none;');
- });
-
- it('shows the refText inside a text element with the deploy-info-text class', () => {
- reducedDeploymentData[0].showDeploymentFlag = true;
- const component = createComponent({
- showDeployInfo: true,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphHeightOffset: 120,
- });
-
- expect(
- component.$el.querySelector('.deploy-info-text').firstChild.nodeValue.trim(),
- ).toEqual(component.refText(reducedDeploymentData[0]));
- });
-
- it('should contain a hidden gradient', () => {
- const component = createComponent({
- showDeployInfo: true,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphHeightOffset: 120,
- });
-
- expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull();
- });
-
- describe('Computed props', () => {
- it('calculatedHeight', () => {
- const component = createComponent({
- showDeployInfo: true,
- deploymentData: reducedDeploymentData,
- graphHeight: 300,
- graphHeightOffset: 120,
- });
-
- expect(component.calculatedHeight).toEqual(180);
- });
- });
- });
-});
diff --git a/spec/javascripts/monitoring/monitoring_flag_spec.js b/spec/javascripts/monitoring/monitoring_flag_spec.js
deleted file mode 100644
index 3861a95ff07..00000000000
--- a/spec/javascripts/monitoring/monitoring_flag_spec.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import Vue from 'vue';
-import MonitoringFlag from '~/monitoring/components/monitoring_flag.vue';
-
-const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringFlag);
-
- return new Component({
- propsData,
- }).$mount();
-};
-
-function getCoordinate(component, selector, coordinate) {
- const coordinateVal = component.$el.querySelector(selector).getAttribute(coordinate);
- return parseInt(coordinateVal, 10);
-}
-
-describe('MonitoringFlag', () => {
- 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,
- });
-
- expect(getCoordinate(component, '.selected-metric-line', 'x1'))
- .toEqual(component.currentXCoordinate);
- expect(getCoordinate(component, '.selected-metric-line', 'x2'))
- .toEqual(component.currentXCoordinate);
- expect(getCoordinate(component, '.circle-metric', 'cx'))
- .toEqual(component.currentXCoordinate);
- expect(getCoordinate(component, '.circle-metric', 'cy'))
- .toEqual(component.currentYCoordinate);
- });
-
- 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 svg = component.$el.querySelector('.rect-text-metric');
- expect(svg.tagName).toEqual('svg');
- expect(parseInt(svg.getAttribute('x'), 10)).toEqual(component.currentFlagPosition);
- });
-
- 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,
- });
-
- expect(component.calculatedHeight).toEqual(180);
- });
- });
-});
diff --git a/spec/javascripts/monitoring/monitoring_legends_spec.js b/spec/javascripts/monitoring/monitoring_legends_spec.js
deleted file mode 100644
index 4c69b81e650..00000000000
--- a/spec/javascripts/monitoring/monitoring_legends_spec.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import Vue from 'vue';
-import MonitoringLegends from '~/monitoring/components/monitoring_legends.vue';
-import measurements from '~/monitoring/utils/measurements';
-
-const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringLegends);
-
- return new Component({
- propsData,
- }).$mount();
-};
-
-function getTextFromNode(component, selector) {
- return component.$el.querySelector(selector).firstChild.nodeValue.trim();
-}
-
-describe('MonitoringLegends', () => {
- describe('Computed props', () => {
- it('textTransform', () => {
- const component = createComponent({
- graphWidth: 500,
- graphHeight: 300,
- margin: measurements.large.margin,
- measurements: measurements.large,
- areaColorRgb: '#f0f0f0',
- legendTitle: 'Title',
- yAxisLabel: 'Values',
- metricUsage: 'Value',
- });
-
- expect(component.textTransform).toContain('translate(15, 120) rotate(-90)');
- });
-
- it('xPosition', () => {
- const component = createComponent({
- graphWidth: 500,
- graphHeight: 300,
- margin: measurements.large.margin,
- measurements: measurements.large,
- areaColorRgb: '#f0f0f0',
- legendTitle: 'Title',
- yAxisLabel: 'Values',
- metricUsage: 'Value',
- });
-
- expect(component.xPosition).toEqual(180);
- });
-
- it('yPosition', () => {
- const component = createComponent({
- graphWidth: 500,
- graphHeight: 300,
- margin: measurements.large.margin,
- measurements: measurements.large,
- areaColorRgb: '#f0f0f0',
- legendTitle: 'Title',
- yAxisLabel: 'Values',
- metricUsage: 'Value',
- });
-
- expect(component.yPosition).toEqual(240);
- });
-
- it('rectTransform', () => {
- const component = createComponent({
- graphWidth: 500,
- graphHeight: 300,
- margin: measurements.large.margin,
- measurements: measurements.large,
- areaColorRgb: '#f0f0f0',
- legendTitle: 'Title',
- yAxisLabel: 'Values',
- metricUsage: 'Value',
- });
-
- expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)');
- });
- });
-
- it('has 2 rect-axis-text rect svg elements', () => {
- const component = createComponent({
- graphWidth: 500,
- graphHeight: 300,
- margin: measurements.large.margin,
- measurements: measurements.large,
- areaColorRgb: '#f0f0f0',
- legendTitle: 'Title',
- yAxisLabel: 'Values',
- metricUsage: 'Value',
- });
-
- expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2);
- });
-
- it('contains text to signal the usage, title and time', () => {
- const component = createComponent({
- graphWidth: 500,
- graphHeight: 300,
- margin: measurements.large.margin,
- measurements: measurements.large,
- areaColorRgb: '#f0f0f0',
- legendTitle: 'Title',
- yAxisLabel: 'Values',
- metricUsage: 'Value',
- });
-
- expect(getTextFromNode(component, '.text-metric-title')).toEqual(component.legendTitle);
- expect(getTextFromNode(component, '.text-metric-usage')).toEqual(component.metricUsage);
- expect(getTextFromNode(component, '.label-axis-text')).toEqual(component.yAxisLabel);
- });
-});
diff --git a/spec/javascripts/monitoring/monitoring_paths_spec.js b/spec/javascripts/monitoring/monitoring_paths_spec.js
new file mode 100644
index 00000000000..d39db945e17
--- /dev/null
+++ b/spec/javascripts/monitoring/monitoring_paths_spec.js
@@ -0,0 +1,34 @@
+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/monitoring_row_spec.js b/spec/javascripts/monitoring/monitoring_row_spec.js
deleted file mode 100644
index a82480e8342..00000000000
--- a/spec/javascripts/monitoring/monitoring_row_spec.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import Vue from 'vue';
-import MonitoringRow from '~/monitoring/components/monitoring_row.vue';
-import { deploymentData, singleRowMetrics } from './mock_data';
-
-const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringRow);
-
- return new Component({
- propsData,
- }).$mount();
-};
-
-describe('MonitoringRow', () => {
- describe('Computed props', () => {
- it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => {
- const component = createComponent({
- rowData: singleRowMetrics,
- updateAspectRatio: false,
- deploymentData,
- });
-
- expect(component.bootstrapClass).toEqual('col-md-6');
- });
-
- it('bootstrapClass is set to col-md-12 when rowData is lower than 2', () => {
- const component = createComponent({
- rowData: [singleRowMetrics[0]],
- updateAspectRatio: false,
- deploymentData,
- });
-
- expect(component.bootstrapClass).toEqual('col-md-12');
- });
- });
-
- it('has one column', () => {
- const component = createComponent({
- rowData: singleRowMetrics,
- updateAspectRatio: false,
- deploymentData,
- });
-
- expect(component.$el.querySelectorAll('.prometheus-svg-container').length)
- .toEqual(component.rowData.length);
- });
-
- it('has two columns', () => {
- const component = createComponent({
- rowData: singleRowMetrics,
- updateAspectRatio: false,
- deploymentData,
- });
-
- expect(component.$el.querySelectorAll('.col-md-6').length)
- .toEqual(component.rowData.length);
- });
-});
diff --git a/spec/javascripts/monitoring/monitoring_spec.js b/spec/javascripts/monitoring/monitoring_spec.js
deleted file mode 100644
index 6c7b691baa4..00000000000
--- a/spec/javascripts/monitoring/monitoring_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import Vue from 'vue';
-import Monitoring from '~/monitoring/components/monitoring.vue';
-import { MonitorMockInterceptor } from './mock_data';
-
-describe('Monitoring', () => {
- const fixtureName = 'environments/metrics/metrics.html.raw';
- let MonitoringComponent;
- let component;
- preloadFixtures(fixtureName);
-
- beforeEach(() => {
- loadFixtures(fixtureName);
- MonitoringComponent = Vue.extend(Monitoring);
- });
-
- describe('no metrics are available yet', () => {
- it('shows a getting started empty state when no metrics are present', () => {
- component = new MonitoringComponent({
- el: document.querySelector('#prometheus-graphs'),
- });
-
- component.$mount();
- expect(component.$el.querySelector('#prometheus-graphs')).toBe(null);
- expect(component.state).toEqual('gettingStarted');
- });
- });
-
- describe('requests information to the server', () => {
- beforeEach(() => {
- document.querySelector('#prometheus-graphs').setAttribute('data-has-metrics', 'true');
- Vue.http.interceptors.push(MonitorMockInterceptor);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, MonitorMockInterceptor);
- });
-
- it('shows up a loading state', (done) => {
- component = new MonitoringComponent({
- el: document.querySelector('#prometheus-graphs'),
- });
- component.$mount();
- Vue.nextTick(() => {
- expect(component.state).toEqual('loading');
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/monitoring/monitoring_state_spec.js b/spec/javascripts/monitoring/monitoring_state_spec.js
deleted file mode 100644
index 4c0c558502f..00000000000
--- a/spec/javascripts/monitoring/monitoring_state_spec.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import Vue from 'vue';
-import MonitoringState from '~/monitoring/components/monitoring_state.vue';
-import { statePaths } from './mock_data';
-
-const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringState);
-
- return new Component({
- propsData,
- }).$mount();
-};
-
-function getTextFromNode(component, selector) {
- return component.$el.querySelector(selector).firstChild.nodeValue.trim();
-}
-
-describe('MonitoringState', () => {
- describe('Computed props', () => {
- it('currentState', () => {
- const component = createComponent({
- selectedState: 'gettingStarted',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- });
-
- expect(component.currentState).toBe(component.states.gettingStarted);
- });
-
- it('buttonPath returns settings path for the state "gettingStarted"', () => {
- const component = createComponent({
- selectedState: 'gettingStarted',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- });
-
- expect(component.buttonPath).toEqual(statePaths.settingsPath);
- expect(component.buttonPath).not.toEqual(statePaths.documentationPath);
- });
-
- it('buttonPath returns documentation path for any of the other states', () => {
- const component = createComponent({
- selectedState: 'loading',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- });
-
- expect(component.buttonPath).toEqual(statePaths.documentationPath);
- expect(component.buttonPath).not.toEqual(statePaths.settingsPath);
- });
-
- it('showButtonDescription returns a description with a link for the unableToConnect state', () => {
- const component = createComponent({
- selectedState: 'unableToConnect',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- });
-
- expect(component.showButtonDescription).toEqual(true);
- });
-
- it('showButtonDescription returns the description without a link for any other state', () => {
- const component = createComponent({
- selectedState: 'loading',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- });
-
- expect(component.showButtonDescription).toEqual(false);
- });
- });
-
- it('should show the gettingStarted state', () => {
- const component = createComponent({
- selectedState: 'gettingStarted',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- });
-
- expect(component.$el.querySelector('svg')).toBeDefined();
- expect(getTextFromNode(component, '.state-title')).toEqual(component.states.gettingStarted.title);
- expect(getTextFromNode(component, '.state-description')).toEqual(component.states.gettingStarted.description);
- expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.gettingStarted.buttonText);
- });
-
- it('should show the loading state', () => {
- const component = createComponent({
- selectedState: 'loading',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- });
-
- expect(component.$el.querySelector('svg')).toBeDefined();
- expect(getTextFromNode(component, '.state-title')).toEqual(component.states.loading.title);
- expect(getTextFromNode(component, '.state-description')).toEqual(component.states.loading.description);
- expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.loading.buttonText);
- });
-
- it('should show the unableToConnect state', () => {
- const component = createComponent({
- selectedState: 'unableToConnect',
- settingsPath: statePaths.settingsPath,
- documentationPath: statePaths.documentationPath,
- });
-
- expect(component.$el.querySelector('svg')).toBeDefined();
- expect(getTextFromNode(component, '.state-title')).toEqual(component.states.unableToConnect.title);
- expect(component.$el.querySelector('.state-description a')).toBeDefined();
- expect(getTextFromNode(component, '.btn-success')).toEqual(component.states.unableToConnect.buttonText);
- });
-});
diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js
index 20c1e6a0005..88aa7659275 100644
--- a/spec/javascripts/monitoring/monitoring_store_spec.js
+++ b/spec/javascripts/monitoring/monitoring_store_spec.js
@@ -5,10 +5,10 @@ describe('MonitoringStore', () => {
this.store = new MonitoringStore();
this.store.storeMetrics(MonitoringMock.data);
- it('contains one group that contains two queries sorted by priority in one row', () => {
+ it('contains one group that contains two queries sorted by priority', () => {
expect(this.store.groups).toBeDefined();
expect(this.store.groups.length).toEqual(1);
- expect(this.store.groups[0].metrics.length).toEqual(1);
+ expect(this.store.groups[0].metrics.length).toEqual(2);
});
it('gets the metrics count for every group', () => {
diff --git a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
new file mode 100644
index 00000000000..3daf6bf82df
--- /dev/null
+++ b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
@@ -0,0 +1,21 @@
+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);
+
+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);
+ });
+
+ it('createTimeSeries returns an array', () => {
+ expect(timeSeries instanceof Array).toEqual(true);
+ expect(timeSeries.length).toEqual(2);
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/issue_comment_form_spec.js
new file mode 100644
index 00000000000..cca5ec887a3
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_comment_form_spec.js
@@ -0,0 +1,134 @@
+import Vue from 'vue';
+import store from '~/notes/stores';
+import issueCommentForm from '~/notes/components/issue_comment_form.vue';
+import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data';
+import { keyboardDownEvent } from '../../issue_show/helpers';
+
+describe('issue_comment_form component', () => {
+ let vm;
+ const Component = Vue.extend(issueCommentForm);
+ let mountComponent;
+
+ beforeEach(() => {
+ mountComponent = () => new Component({
+ store,
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('user is logged in', () => {
+ beforeEach(() => {
+ store.dispatch('setUserData', userDataMock);
+ store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+
+ vm = mountComponent();
+ });
+
+ it('should render user avatar with link', () => {
+ expect(vm.$el.querySelector('.timeline-icon .user-avatar-link').getAttribute('href')).toEqual(userDataMock.path);
+ });
+
+ describe('textarea', () => {
+ it('should render textarea with placeholder', () => {
+ expect(
+ vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'),
+ ).toEqual('Write a comment or drag your files here...');
+ });
+
+ it('should support quick actions', () => {
+ expect(
+ vm.$el.querySelector('.js-main-target-form textarea').getAttribute('data-supports-quick-actions'),
+ ).toEqual('true');
+ });
+
+ it('should link to markdown docs', () => {
+ const { markdownDocsPath } = notesDataMock;
+ expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown');
+ });
+
+ it('should link to quick actions docs', () => {
+ const { quickActionsDocsPath } = notesDataMock;
+ expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions');
+ });
+
+ describe('edit mode', () => {
+ it('should enter edit mode when arrow up is pressed', () => {
+ spyOn(vm, 'editCurrentUserLastNote').and.callThrough();
+ vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
+ vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(38, true));
+
+ expect(vm.editCurrentUserLastNote).toHaveBeenCalled();
+ });
+ });
+
+ describe('event enter', () => {
+ it('should save note when cmd/ctrl+enter is pressed', () => {
+ spyOn(vm, 'handleSave').and.callThrough();
+ vm.$el.querySelector('.js-main-target-form textarea').value = 'Foo';
+ vm.$el.querySelector('.js-main-target-form textarea').dispatchEvent(keyboardDownEvent(13, true));
+
+ expect(vm.handleSave).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('actions', () => {
+ it('should be possible to close the issue', () => {
+ expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close issue');
+ });
+
+ it('should render comment button as disabled', () => {
+ expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual('disabled');
+ });
+
+ it('should enable comment button if it has note', (done) => {
+ vm.note = 'Foo';
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.js-comment-submit-button').getAttribute('disabled')).toEqual(null);
+ done();
+ });
+ });
+
+ it('should update buttons texts when it has note', (done) => {
+ vm.note = 'Foo';
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Comment & close issue');
+ expect(vm.$el.querySelector('.js-note-discard')).toBeDefined();
+ done();
+ });
+ });
+ });
+
+ describe('issue is confidential', () => {
+ it('shows information warning', (done) => {
+ store.dispatch('setIssueData', Object.assign(issueDataMock, { confidential: true }));
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined();
+ done();
+ });
+ });
+ });
+ });
+
+ describe('user is not logged in', () => {
+ beforeEach(() => {
+ store.dispatch('setUserData', null);
+ store.dispatch('setIssueData', loggedOutIssueData);
+ store.dispatch('setNotesData', notesDataMock);
+
+ vm = mountComponent();
+ });
+
+ it('should render signed out widget', () => {
+ expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply');
+ });
+
+ it('should not render submission form', () => {
+ expect(vm.$el.querySelector('textarea')).toEqual(null);
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/issue_discussion_spec.js
new file mode 100644
index 00000000000..05c6b57f93e
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_discussion_spec.js
@@ -0,0 +1,50 @@
+import Vue from 'vue';
+import store from '~/notes/stores';
+import issueDiscussion from '~/notes/components/issue_discussion.vue';
+import { issueDataMock, discussionMock, notesDataMock } from '../mock_data';
+
+describe('issue_discussion component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(issueDiscussion);
+
+ store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+
+ vm = new Component({
+ store,
+ propsData: {
+ note: discussionMock,
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render user avatar', () => {
+ expect(vm.$el.querySelector('.user-avatar-link')).toBeDefined();
+ });
+
+ it('should render discussion header', () => {
+ expect(vm.$el.querySelector('.discussion-header')).toBeDefined();
+ expect(vm.$el.querySelectorAll('.notes li').length).toEqual(discussionMock.notes.length);
+ });
+
+ describe('actions', () => {
+ it('should render reply button', () => {
+ expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual('Reply...');
+ });
+
+ it('should toggle reply form', (done) => {
+ vm.$el.querySelector('.js-vue-discussion-reply').click();
+ Vue.nextTick(() => {
+ expect(vm.$refs.noteForm).toBeDefined();
+ expect(vm.isReplying).toEqual(true);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_note_actions_spec.js b/spec/javascripts/notes/components/issue_note_actions_spec.js
new file mode 100644
index 00000000000..7bcc061f167
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_note_actions_spec.js
@@ -0,0 +1,91 @@
+import Vue from 'vue';
+import store from '~/notes/stores';
+import issueActions from '~/notes/components/issue_note_actions.vue';
+import { userDataMock } from '../mock_data';
+
+describe('issse_note_actions component', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(issueActions);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('user is logged in', () => {
+ let props;
+
+ beforeEach(() => {
+ props = {
+ accessLevel: 'Master',
+ authorId: 26,
+ canDelete: true,
+ canEdit: true,
+ canReportAsAbuse: true,
+ noteId: 539,
+ reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
+ };
+
+ store.dispatch('setUserData', userDataMock);
+
+ vm = new Component({
+ store,
+ propsData: props,
+ }).$mount();
+ });
+
+ it('should render access level badge', () => {
+ expect(vm.$el.querySelector('.note-role').textContent.trim()).toEqual(props.accessLevel);
+ });
+
+ it('should render emoji link', () => {
+ expect(vm.$el.querySelector('.js-add-award')).toBeDefined();
+ });
+
+ describe('actions dropdown', () => {
+ it('should be possible to edit the comment', () => {
+ expect(vm.$el.querySelector('.js-note-edit')).toBeDefined();
+ });
+
+ it('should be possible to report as abuse', () => {
+ expect(vm.$el.querySelector(`a[href="${props.reportAbusePath}"]`)).toBeDefined();
+ });
+
+ it('should be possible to delete comment', () => {
+ expect(vm.$el.querySelector('.js-note-delete')).toBeDefined();
+ });
+ });
+ });
+
+ describe('user is not logged in', () => {
+ let props;
+
+ beforeEach(() => {
+ store.dispatch('setUserData', {});
+ props = {
+ accessLevel: 'Master',
+ authorId: 26,
+ canDelete: false,
+ canEdit: false,
+ canReportAsAbuse: false,
+ noteId: 539,
+ reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26',
+ };
+ vm = new Component({
+ store,
+ propsData: props,
+ }).$mount();
+ });
+
+ it('should not render emoji link', () => {
+ expect(vm.$el.querySelector('.js-add-award')).toEqual(null);
+ });
+
+ it('should not render actions dropdown', () => {
+ expect(vm.$el.querySelector('.more-actions')).toEqual(null);
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/issue_note_app_spec.js
new file mode 100644
index 00000000000..22e91c4c40f
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_note_app_spec.js
@@ -0,0 +1,255 @@
+import Vue from 'vue';
+import issueNotesApp from '~/notes/components/issue_notes_app.vue';
+import service from '~/notes/services/issue_notes_service';
+import * as mockData from '../mock_data';
+
+describe('issue_note_app', () => {
+ let mountComponent;
+ let vm;
+
+ const individualNoteInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(mockData.individualNoteServerResponse), {
+ status: 200,
+ }));
+ };
+
+ const discussionNoteInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(mockData.discussionNoteServerResponse), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ const IssueNotesApp = Vue.extend(issueNotesApp);
+
+ mountComponent = (data) => {
+ const props = data || {
+ issueData: mockData.issueDataMock,
+ notesData: mockData.notesDataMock,
+ userData: mockData.userDataMock,
+ };
+
+ return new IssueNotesApp({
+ propsData: props,
+ }).$mount();
+ };
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('set data', () => {
+ const responseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(responseInterceptor);
+ vm = mountComponent();
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor);
+ });
+
+ it('should set notes data', () => {
+ expect(vm.$store.state.notesData).toEqual(mockData.notesDataMock);
+ });
+
+ it('should set issue data', () => {
+ expect(vm.$store.state.issueData).toEqual(mockData.issueDataMock);
+ });
+
+ it('should set user data', () => {
+ expect(vm.$store.state.userData).toEqual(mockData.userDataMock);
+ });
+
+ it('should fetch notes', () => {
+ expect(vm.$store.state.notes).toEqual([]);
+ });
+ });
+
+ describe('render', () => {
+ beforeEach(() => {
+ Vue.http.interceptors.push(individualNoteInterceptor);
+ vm = mountComponent();
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor);
+ });
+
+ it('should render list of notes', (done) => {
+ const note = mockData.individualNoteServerResponse[0].notes[0];
+
+ setTimeout(() => {
+ expect(
+ vm.$el.querySelector('.main-notes-list .note-header-author-name').textContent.trim(),
+ ).toEqual(note.author.name);
+
+ expect(vm.$el.querySelector('.main-notes-list .note-text').innerHTML).toEqual(note.note_html);
+ done();
+ }, 0);
+ });
+
+ it('should render form', () => {
+ expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM');
+ expect(
+ vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'),
+ ).toEqual('Write a comment or drag your files here...');
+ });
+
+ it('should render form comment button as disabled', () => {
+ expect(
+ vm.$el.querySelector('.js-note-new-discussion').getAttribute('disabled'),
+ ).toEqual('disabled');
+ });
+ });
+
+ describe('while fetching data', () => {
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
+ it('should render loading icon', () => {
+ expect(vm.$el.querySelector('.js-loading')).toBeDefined();
+ });
+
+ it('should render form', () => {
+ expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM');
+ expect(
+ vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'),
+ ).toEqual('Write a comment or drag your files here...');
+ });
+ });
+
+ describe('update note', () => {
+ describe('individual note', () => {
+ beforeEach(() => {
+ Vue.http.interceptors.push(individualNoteInterceptor);
+ spyOn(service, 'updateNote').and.callFake(() => Promise.resolve());
+ vm = mountComponent();
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor);
+ });
+
+ it('renders edit form', (done) => {
+ setTimeout(() => {
+ vm.$el.querySelector('.js-note-edit').click();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined();
+ done();
+ });
+ }, 0);
+ });
+
+ it('calls the service to update the note', (done) => {
+ setTimeout(() => {
+ vm.$el.querySelector('.js-note-edit').click();
+ Vue.nextTick(() => {
+ vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
+ vm.$el.querySelector('.js-vue-issue-save').click();
+
+ expect(service.updateNote).toHaveBeenCalled();
+ done();
+ });
+ }, 0);
+ });
+ });
+
+ describe('dicussion note', () => {
+ beforeEach(() => {
+ Vue.http.interceptors.push(discussionNoteInterceptor);
+ spyOn(service, 'updateNote').and.callFake(() => Promise.resolve());
+ vm = mountComponent();
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, discussionNoteInterceptor);
+ });
+
+ it('renders edit form', (done) => {
+ setTimeout(() => {
+ vm.$el.querySelector('.js-note-edit').click();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.js-vue-issue-note-form')).toBeDefined();
+ done();
+ });
+ }, 0);
+ });
+
+ it('updates the note and resets the edit form', (done) => {
+ setTimeout(() => {
+ vm.$el.querySelector('.js-note-edit').click();
+ Vue.nextTick(() => {
+ vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
+ vm.$el.querySelector('.js-vue-issue-save').click();
+
+ expect(service.updateNote).toHaveBeenCalled();
+ done();
+ });
+ }, 0);
+ });
+ });
+ });
+
+ describe('new note form', () => {
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
+ it('should render markdown docs url', () => {
+ const { markdownDocsPath } = mockData.notesDataMock;
+ expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown');
+ });
+
+ it('should render quick action docs url', () => {
+ const { quickActionsDocsPath } = mockData.notesDataMock;
+ expect(vm.$el.querySelector(`a[href="${quickActionsDocsPath}"]`).textContent.trim()).toEqual('quick actions');
+ });
+ });
+
+ describe('edit form', () => {
+ beforeEach(() => {
+ Vue.http.interceptors.push(individualNoteInterceptor);
+ vm = mountComponent();
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor);
+ });
+
+ it('should render markdown docs url', (done) => {
+ setTimeout(() => {
+ vm.$el.querySelector('.js-note-edit').click();
+ const { markdownDocsPath } = mockData.notesDataMock;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector(`.edit-note a[href="${markdownDocsPath}"]`).textContent.trim(),
+ ).toEqual('Markdown is supported');
+ done();
+ });
+ }, 0);
+ });
+
+ it('should not render quick actions docs url', (done) => {
+ setTimeout(() => {
+ vm.$el.querySelector('.js-note-edit').click();
+ const { quickActionsDocsPath } = mockData.notesDataMock;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector(`.edit-note a[href="${quickActionsDocsPath}"]`),
+ ).toEqual(null);
+ done();
+ });
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_note_attachment_spec.js b/spec/javascripts/notes/components/issue_note_attachment_spec.js
new file mode 100644
index 00000000000..8f33b874ad6
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_note_attachment_spec.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import issueNoteAttachment from '~/notes/components/issue_note_attachment.vue';
+
+describe('issue note attachment', () => {
+ it('should render properly', () => {
+ const props = {
+ attachment: {
+ filename: 'dk.png',
+ image: true,
+ url: '/dk.png',
+ },
+ };
+
+ const Component = Vue.extend(issueNoteAttachment);
+ const vm = new Component({
+ propsData: props,
+ }).$mount();
+
+ expect(vm.$el.classList.contains('note-attachment')).toBeTruthy();
+ expect(vm.$el.querySelector('img').src).toContain(props.attachment.url);
+ expect(vm.$el.querySelector('a').href).toContain(props.attachment.url);
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_note_awards_list_spec.js b/spec/javascripts/notes/components/issue_note_awards_list_spec.js
new file mode 100644
index 00000000000..3b6c34f1494
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_note_awards_list_spec.js
@@ -0,0 +1,56 @@
+import Vue from 'vue';
+import store from '~/notes/stores';
+import awardsNote from '~/notes/components/issue_note_awards_list.vue';
+import { issueDataMock, notesDataMock } from '../mock_data';
+
+describe('issue_note_awards_list component', () => {
+ let vm;
+ let awardsMock;
+
+ beforeEach(() => {
+ const Component = Vue.extend(awardsNote);
+
+ store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+ awardsMock = [
+ {
+ name: 'flag_tz',
+ user: { id: 1, name: 'Administrator', username: 'root' },
+ },
+ {
+ name: 'cartwheel_tone3',
+ user: { id: 12, name: 'Bobbie Stehr', username: 'erin' },
+ },
+ ];
+
+ vm = new Component({
+ store,
+ propsData: {
+ awards: awardsMock,
+ noteAuthorId: 2,
+ noteId: 545,
+ toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji',
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render awarded emojis', () => {
+ expect(vm.$el.querySelector('.js-awards-block button [data-name="flag_tz"]')).toBeDefined();
+ expect(vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]')).toBeDefined();
+ });
+
+ it('should be possible to remove awareded emoji', () => {
+ spyOn(vm, 'handleAward').and.callThrough();
+ vm.$el.querySelector('.js-awards-block button').click();
+
+ expect(vm.handleAward).toHaveBeenCalledWith('flag_tz');
+ });
+
+ it('should be possible to add new emoji', () => {
+ expect(vm.$el.querySelector('.js-add-award')).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_note_body_spec.js b/spec/javascripts/notes/components/issue_note_body_spec.js
new file mode 100644
index 00000000000..81f07ed47cc
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_note_body_spec.js
@@ -0,0 +1,46 @@
+
+import Vue from 'vue';
+import store from '~/notes/stores';
+import noteBody from '~/notes/components/issue_note_body.vue';
+import { issueDataMock, notesDataMock, note } from '../mock_data';
+
+describe('issue_note_body component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(noteBody);
+
+ store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+
+ vm = new Component({
+ store,
+ propsData: {
+ note,
+ canEdit: true,
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render the note', () => {
+ expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html);
+ });
+
+ it('should be render form if user is editing', (done) => {
+ vm.isEditing = true;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('textarea.js-task-list-field')).toBeDefined();
+ done();
+ });
+ });
+
+ it('should render awards list', () => {
+ expect(vm.$el.querySelector('.js-awards-block button [data-name="baseball"]')).toBeDefined();
+ expect(vm.$el.querySelector('.js-awards-block button [data-name="bath_tone3"]')).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_note_edited_text_spec.js b/spec/javascripts/notes/components/issue_note_edited_text_spec.js
new file mode 100644
index 00000000000..6603241eb64
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_note_edited_text_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import issueNoteEditedText from '~/notes/components/issue_note_edited_text.vue';
+
+describe('issue_note_edited_text', () => {
+ let vm;
+ let props;
+
+ beforeEach(() => {
+ const Component = Vue.extend(issueNoteEditedText);
+ props = {
+ actionText: 'Edited',
+ className: 'foo-bar',
+ editedAt: '2017-08-04T09:52:31.062Z',
+ editedBy: {
+ avatar_url: 'path',
+ id: 1,
+ name: 'Root',
+ path: '/root',
+ state: 'active',
+ username: 'root',
+ },
+ };
+
+ vm = new Component({
+ propsData: props,
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render block with provided className', () => {
+ expect(vm.$el.className).toEqual(props.className);
+ });
+
+ it('should render provided actionText', () => {
+ expect(vm.$el.textContent).toContain(props.actionText);
+ });
+
+ it('should render provided user information', () => {
+ const authorLink = vm.$el.querySelector('.js-vue-author');
+
+ expect(authorLink.getAttribute('href')).toEqual(props.editedBy.path);
+ expect(authorLink.textContent.trim()).toEqual(props.editedBy.name);
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/issue_note_form_spec.js
new file mode 100644
index 00000000000..a90dbcb72b5
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_note_form_spec.js
@@ -0,0 +1,112 @@
+import Vue from 'vue';
+import store from '~/notes/stores';
+import issueNoteForm from '~/notes/components/issue_note_form.vue';
+import { issueDataMock, notesDataMock } from '../mock_data';
+import { keyboardDownEvent } from '../../issue_show/helpers';
+
+describe('issue_note_form component', () => {
+ let vm;
+ let props;
+
+ beforeEach(() => {
+ const Component = Vue.extend(issueNoteForm);
+
+ store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+
+ props = {
+ isEditing: false,
+ noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.',
+ noteId: 545,
+ };
+
+ vm = new Component({
+ store,
+ propsData: props,
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('conflicts editing', () => {
+ it('should show conflict message if note changes outside the component', (done) => {
+ vm.isEditing = true;
+ vm.noteBody = 'Foo';
+ const message = 'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.';
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.js-conflict-edit-warning').textContent.replace(/\s+/g, ' ').trim(),
+ ).toEqual(message);
+ done();
+ });
+ });
+ });
+
+ describe('form', () => {
+ it('should render text area with placeholder', () => {
+ expect(
+ vm.$el.querySelector('textarea').getAttribute('placeholder'),
+ ).toEqual('Write a comment or drag your files here...');
+ });
+
+ it('should link to markdown docs', () => {
+ const { markdownDocsPath } = notesDataMock;
+ expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual('Markdown');
+ });
+
+ describe('keyboard events', () => {
+ describe('up', () => {
+ it('should ender edit mode', () => {
+ spyOn(vm, 'editMyLastNote').and.callThrough();
+ vm.$el.querySelector('textarea').value = 'Foo';
+ vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(38, true));
+
+ expect(vm.editMyLastNote).toHaveBeenCalled();
+ });
+ });
+
+ describe('enter', () => {
+ it('should submit note', () => {
+ spyOn(vm, 'handleUpdate').and.callThrough();
+ vm.$el.querySelector('textarea').value = 'Foo';
+ vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, true));
+
+ expect(vm.handleUpdate).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('actions', () => {
+ it('should be possible to cancel', (done) => {
+ spyOn(vm, 'cancelHandler').and.callThrough();
+ vm.isEditing = true;
+
+ Vue.nextTick(() => {
+ vm.$el.querySelector('.note-edit-cancel').click();
+
+ Vue.nextTick(() => {
+ expect(vm.cancelHandler).toHaveBeenCalled();
+ done();
+ });
+ });
+ });
+
+ it('should be possible to update the note', (done) => {
+ vm.isEditing = true;
+
+ Vue.nextTick(() => {
+ vm.$el.querySelector('textarea').value = 'Foo';
+ vm.$el.querySelector('.js-vue-issue-save').click();
+
+ Vue.nextTick(() => {
+ expect(vm.isSubmitting).toEqual(true);
+ done();
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_note_header_spec.js b/spec/javascripts/notes/components/issue_note_header_spec.js
new file mode 100644
index 00000000000..83ea18508ae
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_note_header_spec.js
@@ -0,0 +1,94 @@
+import Vue from 'vue';
+import issueNoteHeader from '~/notes/components/issue_note_header.vue';
+import store from '~/notes/stores';
+
+describe('issue_note_header component', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(issueNoteHeader);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('individual note', () => {
+ beforeEach(() => {
+ vm = new Component({
+ store,
+ propsData: {
+ actionText: 'commented',
+ actionTextHtml: '',
+ author: {
+ avatar_url: null,
+ id: 1,
+ name: 'Root',
+ path: '/root',
+ state: 'active',
+ username: 'root',
+ },
+ createdAt: '2017-08-02T10:51:58.559Z',
+ includeToggle: false,
+ noteId: 1394,
+ },
+ }).$mount();
+ });
+
+ it('should render user information', () => {
+ expect(
+ vm.$el.querySelector('.note-header-author-name').textContent.trim(),
+ ).toEqual('Root');
+ expect(
+ vm.$el.querySelector('.note-header-info a').getAttribute('href'),
+ ).toEqual('/root');
+ });
+
+ it('should render timestamp link', () => {
+ expect(vm.$el.querySelector('a[href="#note_1394"]')).toBeDefined();
+ });
+ });
+
+ describe('discussion', () => {
+ beforeEach(() => {
+ vm = new Component({
+ store,
+ propsData: {
+ actionText: 'started a discussion',
+ actionTextHtml: '',
+ author: {
+ avatar_url: null,
+ id: 1,
+ name: 'Root',
+ path: '/root',
+ state: 'active',
+ username: 'root',
+ },
+ createdAt: '2017-08-02T10:51:58.559Z',
+ includeToggle: true,
+ noteId: 1395,
+ },
+ }).$mount();
+ });
+
+ it('should render toggle button', () => {
+ expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined();
+ });
+
+ it('should toggle the disucssion icon', (done) => {
+ expect(
+ vm.$el.querySelector('.js-vue-toggle-button i').classList.contains('fa-chevron-up'),
+ ).toEqual(true);
+
+ vm.$el.querySelector('.js-vue-toggle-button').click();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.js-vue-toggle-button i').classList.contains('fa-chevron-down'),
+ ).toEqual(true);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js b/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js
new file mode 100644
index 00000000000..f20d9ce9268
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import issueNoteSignedOut from '~/notes/components/issue_note_signed_out_widget.vue';
+import store from '~/notes/stores';
+import { notesDataMock } from '../mock_data';
+
+describe('issue_note_signed_out_widget component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(issueNoteSignedOut);
+ store.dispatch('setNotesData', notesDataMock);
+
+ vm = new Component({
+ store,
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render sign in link provided in the store', () => {
+ expect(
+ vm.$el.querySelector(`a[href="${notesDataMock.newSessionPath}"]`).textContent,
+ ).toEqual('sign in');
+ });
+
+ it('should render register link provided in the store', () => {
+ expect(
+ vm.$el.querySelector(`a[href="${notesDataMock.registerPath}"]`).textContent,
+ ).toEqual('register');
+ });
+
+ it('should render information text', () => {
+ expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('Please register or sign in to reply');
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_note_spec.js b/spec/javascripts/notes/components/issue_note_spec.js
new file mode 100644
index 00000000000..7ef85d5b4f0
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_note_spec.js
@@ -0,0 +1,44 @@
+
+import Vue from 'vue';
+import store from '~/notes/stores';
+import issueNote from '~/notes/components/issue_note.vue';
+import { issueDataMock, notesDataMock, note } from '../mock_data';
+
+describe('issue_note', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(issueNote);
+
+ store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+
+ vm = new Component({
+ store,
+ propsData: {
+ note,
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render user information', () => {
+ expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(note.author.avatar_url);
+ });
+
+ it('should render note header content', () => {
+ expect(vm.$el.querySelector('.note-header .note-header-author-name').textContent.trim()).toEqual(note.author.name);
+ expect(vm.$el.querySelector('.note-header .note-headline-meta').textContent.trim()).toContain('commented');
+ });
+
+ it('should render note actions', () => {
+ expect(vm.$el.querySelector('.note-actions')).toBeDefined();
+ });
+
+ it('should render issue body', () => {
+ expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html);
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_placeholder_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_note_spec.js
new file mode 100644
index 00000000000..6e5275087f3
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_placeholder_note_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import issuePlaceholderNote from '~/notes/components/issue_placeholder_note.vue';
+import store from '~/notes/stores';
+import { userDataMock } from '../mock_data';
+
+describe('issue placeholder system note component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(issuePlaceholderNote);
+ store.dispatch('setUserData', userDataMock);
+ vm = new Component({
+ store,
+ propsData: { note: { body: 'Foo' } },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('user information', () => {
+ it('should render user avatar with link', () => {
+ expect(vm.$el.querySelector('.user-avatar-link').getAttribute('href')).toEqual(userDataMock.path);
+ expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual(userDataMock.avatar_url);
+ });
+ });
+
+ describe('note content', () => {
+ it('should render note header information', () => {
+ expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual(userDataMock.path);
+ expect(vm.$el.querySelector('.note-header-info .note-headline-light').textContent.trim()).toEqual(`@${userDataMock.username}`);
+ });
+
+ it('should render note body', () => {
+ expect(vm.$el.querySelector('.note-text p').textContent.trim()).toEqual('Foo');
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js b/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js
new file mode 100644
index 00000000000..d508a49f710
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_placeholder_system_note_spec.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import placeholderSystemNote from '~/notes/components/issue_placeholder_system_note.vue';
+
+describe('issue placeholder system note component', () => {
+ let mountComponent;
+ beforeEach(() => {
+ const PlaceholderSystemNote = Vue.extend(placeholderSystemNote);
+
+ mountComponent = props => new PlaceholderSystemNote({
+ propsData: {
+ note: {
+ body: props,
+ },
+ },
+ }).$mount();
+ });
+
+ it('should render system note placeholder with plain text', () => {
+ const vm = mountComponent('This is a placeholder');
+
+ expect(vm.$el.tagName).toEqual('LI');
+ expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder');
+ });
+});
diff --git a/spec/javascripts/notes/components/issue_system_note_spec.js b/spec/javascripts/notes/components/issue_system_note_spec.js
new file mode 100644
index 00000000000..c317ce32716
--- /dev/null
+++ b/spec/javascripts/notes/components/issue_system_note_spec.js
@@ -0,0 +1,53 @@
+import Vue from 'vue';
+import issueSystemNote from '~/notes/components/issue_system_note.vue';
+import store from '~/notes/stores';
+
+describe('issue system note', () => {
+ let vm;
+ let props;
+
+ beforeEach(() => {
+ props = {
+ note: {
+ id: 1424,
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'path',
+ path: '/root',
+ },
+ note_html: '<p dir="auto">closed</p>',
+ system_note_icon_name: 'icon_status_closed',
+ created_at: '2017-08-02T10:51:58.559Z',
+ },
+ };
+
+ store.dispatch('setTargetNoteHash', `note_${props.note.id}`);
+
+ const Component = Vue.extend(issueSystemNote);
+ vm = new Component({
+ store,
+ propsData: props,
+ }).$mount();
+ });
+
+ it('should render a list item with correct id', () => {
+ expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`);
+ });
+
+ it('should render target class is note is target note', () => {
+ expect(vm.$el.classList).toContain('target');
+ });
+
+ it('should render svg icon', () => {
+ expect(vm.$el.querySelector('.timeline-icon svg')).toBeDefined();
+ });
+
+ it('should render note header component', () => {
+ expect(
+ vm.$el.querySelector('.system-note-message').innerHTML,
+ ).toEqual(props.note.note_html);
+ });
+});
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
new file mode 100644
index 00000000000..89ba3a002b7
--- /dev/null
+++ b/spec/javascripts/notes/mock_data.js
@@ -0,0 +1,449 @@
+/* eslint-disable */
+export const notesDataMock = {
+ discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json',
+ lastFetchedAt: '1501862675',
+ markdownDocsPath: '/help/user/markdown',
+ newSessionPath: '/users/sign_in?redirect_to_referer=yes',
+ notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes',
+ quickActionsDocsPath: '/help/user/project/quick_actions',
+ registerPath: '/users/sign_in?redirect_to_referer=yes#register-pane',
+};
+
+export const userDataMock = {
+ avatar_url: 'mock_path',
+ id: 1,
+ name: 'Root',
+ path: '/root',
+ state: 'active',
+ username: 'root',
+};
+
+export const issueDataMock = {
+ assignees: [],
+ author_id: 1,
+ branch_name: null,
+ confidential: false,
+ create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue',
+ created_at: '2017-02-07T10:11:18.395Z',
+ current_user: {
+ can_create_note: true,
+ can_update: true,
+ },
+ deleted_at: null,
+ description: '',
+ due_date: null,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ id: 98,
+ iid: 26,
+ labels: [],
+ lock_version: null,
+ milestone: null,
+ milestone_id: null,
+ moved_to_id: null,
+ preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
+ project_id: 2,
+ state: 'opened',
+ time_estimate: 0,
+ title: '14',
+ total_time_spent: 0,
+ updated_at: '2017-08-04T09:53:01.226Z',
+ updated_by_id: 1,
+ web_url: '/gitlab-org/gitlab-ce/issues/26',
+};
+
+export const lastFetchedAt = '1501862675';
+
+export const individualNote = {
+ expanded: true,
+ id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ individual_note: true,
+ notes: [{
+ id: 1390,
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: 'test',
+ path: '/root',
+ },
+ created_at: '2017-08-01T17: 09: 33.762Z',
+ updated_at: '2017-08-01T17: 09: 33.762Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: null,
+ human_access: 'Owner',
+ note: 'sdfdsaf',
+ note_html: '<p dir=\'auto\'>sdfdsaf</p>',
+ current_user: { can_edit: true },
+ discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+ emoji_awardable: true,
+ award_emoji: [
+ { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } },
+ { name: 'art', user: { id: 1, name: 'Root', username: 'root' } },
+ ],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji',
+ report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/1390',
+ }],
+ reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+};
+
+export const note = {
+ "id": 546,
+ "attachment": {
+ "url": null,
+ "filename": null,
+ "image": false
+ },
+ "author": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "path": "/root"
+ },
+ "created_at": "2017-08-10T15:24:03.087Z",
+ "updated_at": "2017-08-10T15:24:03.087Z",
+ "system": false,
+ "noteable_id": 67,
+ "noteable_type": "Issue",
+ "noteable_iid": 7,
+ "type": null,
+ "human_access": "Owner",
+ "note": "Vel id placeat reprehenderit sit numquam.",
+ "note_html": "<p dir=\"auto\">Vel id placeat reprehenderit sit numquam.</p>",
+ "current_user": {
+ "can_edit": true
+ },
+ "discussion_id": "d3842a451b7f3d9a5dfce329515127b2d29a4cd0",
+ "emoji_awardable": true,
+ "award_emoji": [{
+ "name": "baseball",
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root"
+ }
+ }, {
+ "name": "bath_tone3",
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root"
+ }
+ }],
+ "toggle_award_path": "/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji",
+ "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1",
+ "path": "/gitlab-org/gitlab-ce/notes/546"
+ }
+
+export const discussionMock = {
+ id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ reply_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ expanded: true,
+ notes: [{
+ id: 1395,
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-02T10:51:58.559Z',
+ updated_at: '2017-08-02T10:51:58.559Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'THIS IS A DICUSSSION!',
+ note_html: '<p dir=\'auto\'>THIS IS A DICUSSSION!</p>',
+ current_user: {
+ can_edit: true,
+ },
+ discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji',
+ report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/1395',
+ }, {
+ id: 1396,
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-02T10:56:50.980Z',
+ updated_at: '2017-08-03T14:19:35.691Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'sadfasdsdgdsf',
+ note_html: '<p dir=\'auto\'>sadfasdsdgdsf</p>',
+ last_edited_at: '2017-08-03T14:19:35.691Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ },
+ discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji',
+ report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/1396',
+ }, {
+ id: 1437,
+ attachment: {
+ url: null,
+ filename: null,
+ image: false,
+ },
+ author: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ created_at: '2017-08-03T18:11:18.780Z',
+ updated_at: '2017-08-04T09:52:31.062Z',
+ system: false,
+ noteable_id: 98,
+ noteable_type: 'Issue',
+ type: 'DiscussionNote',
+ human_access: 'Owner',
+ note: 'adsfasf Should disappear',
+ note_html: '<p dir=\'auto\'>adsfasf Should disappear</p>',
+ last_edited_at: '2017-08-04T09:52:31.062Z',
+ last_edited_by: {
+ id: 1,
+ name: 'Root',
+ username: 'root',
+ state: 'active',
+ avatar_url: null,
+ path: '/root',
+ },
+ current_user: {
+ can_edit: true,
+ },
+ discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+ emoji_awardable: true,
+ award_emoji: [],
+ toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji',
+ report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1',
+ path: '/gitlab-org/gitlab-ce/notes/1437',
+ }],
+ individual_note: false,
+};
+
+export const loggedOutIssueData = {
+ "id": 98,
+ "iid": 26,
+ "author_id": 1,
+ "description": "",
+ "lock_version": 1,
+ "milestone_id": null,
+ "state": "opened",
+ "title": "asdsa",
+ "updated_by_id": 1,
+ "created_at": "2017-02-07T10:11:18.395Z",
+ "updated_at": "2017-08-08T10:22:51.564Z",
+ "deleted_at": null,
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null,
+ "milestone": null,
+ "labels": [],
+ "branch_name": null,
+ "confidential": false,
+ "assignees": [{
+ "id": 1,
+ "name": "Root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": null,
+ "web_url": "http://localhost:3000/root"
+ }],
+ "due_date": null,
+ "moved_to_id": null,
+ "project_id": 2,
+ "web_url": "/gitlab-org/gitlab-ce/issues/26",
+ "current_user": {
+ "can_create_note": false,
+ "can_update": false
+ },
+ "create_note_path": "/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue",
+ "preview_note_path": "/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue"
+}
+
+export const individualNoteServerResponse = [{
+ "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
+ "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
+ "expanded": true,
+ "notes": [{
+ "id": 1390,
+ "attachment": {
+ "url": null,
+ "filename": null,
+ "image": false
+ },
+ "author": {
+ "id": 1,
+ "name": "Root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": null,
+ "path": "/root"
+ },
+ "created_at": "2017-08-01T17:09:33.762Z",
+ "updated_at": "2017-08-01T17:09:33.762Z",
+ "system": false,
+ "noteable_id": 98,
+ "noteable_type": "Issue",
+ "type": null,
+ "human_access": "Owner",
+ "note": "sdfdsaf",
+ "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e",
+ "current_user": {
+ "can_edit": true
+ },
+ "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
+ "emoji_awardable": true,
+ "award_emoji": [{
+ "name": "baseball",
+ "user": {
+ "id": 1,
+ "name": "Root",
+ "username": "root"
+ }
+ }, {
+ "name": "art",
+ "user": {
+ "id": 1,
+ "name": "Root",
+ "username": "root"
+ }
+ }],
+ "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji",
+ "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1",
+ "path": "/gitlab-org/gitlab-ce/notes/1390"
+ }],
+ "individual_note": true
+ }, {
+ "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
+ "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
+ "expanded": true,
+ "notes": [{
+ "id": 1391,
+ "attachment": {
+ "url": null,
+ "filename": null,
+ "image": false
+ },
+ "author": {
+ "id": 1,
+ "name": "Root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": null,
+ "path": "/root"
+ },
+ "created_at": "2017-08-02T10:51:38.685Z",
+ "updated_at": "2017-08-02T10:51:38.685Z",
+ "system": false,
+ "noteable_id": 98,
+ "noteable_type": "Issue",
+ "type": null,
+ "human_access": "Owner",
+ "note": "New note!",
+ "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e",
+ "current_user": {
+ "can_edit": true
+ },
+ "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
+ "emoji_awardable": true,
+ "award_emoji": [],
+ "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji",
+ "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1",
+ "path": "/gitlab-org/gitlab-ce/notes/1391"
+ }],
+ "individual_note": true
+}];
+
+export const discussionNoteServerResponse = [{
+ "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
+ "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
+ "expanded": true,
+ "notes": [{
+ "id": 1471,
+ "attachment": {
+ "url": null,
+ "filename": null,
+ "image": false
+ },
+ "author": {
+ "id": 1,
+ "name": "Root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": null,
+ "path": "/root"
+ },
+ "created_at": "2017-08-08T16:53:00.666Z",
+ "updated_at": "2017-08-08T16:53:00.666Z",
+ "system": false,
+ "noteable_id": 124,
+ "noteable_type": "Issue",
+ "noteable_iid": 29,
+ "type": "DiscussionNote",
+ "human_access": "Owner",
+ "note": "Adding a comment",
+ "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e",
+ "current_user": {
+ "can_edit": true
+ },
+ "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
+ "emoji_awardable": true,
+ "award_emoji": [],
+ "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji",
+ "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1",
+ "path": "/gitlab-org/gitlab-ce/notes/1471"
+ }],
+ "individual_note": false
+}];
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
new file mode 100644
index 00000000000..72d362acb2f
--- /dev/null
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -0,0 +1,62 @@
+
+import * as actions from '~/notes/stores/actions';
+import testAction from './helpers';
+import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
+
+describe('Actions Notes Store', () => {
+ describe('setNotesData', () => {
+ it('should set received notes data', (done) => {
+ testAction(actions.setNotesData, null, { notesData: {} }, [
+ { type: 'SET_NOTES_DATA', payload: notesDataMock },
+ ], done);
+ });
+ });
+
+ describe('setIssueData', () => {
+ it('should set received issue data', (done) => {
+ testAction(actions.setIssueData, null, { issueData: {} }, [
+ { type: 'SET_ISSUE_DATA', payload: issueDataMock },
+ ], done);
+ });
+ });
+
+ describe('setUserData', () => {
+ it('should set received user data', (done) => {
+ testAction(actions.setUserData, null, { userData: {} }, [
+ { type: 'SET_USER_DATA', payload: userDataMock },
+ ], done);
+ });
+ });
+
+ describe('setLastFetchedAt', () => {
+ it('should set received timestamp', (done) => {
+ testAction(actions.setLastFetchedAt, null, { lastFetchedAt: {} }, [
+ { type: 'SET_LAST_FETCHED_AT', payload: 'timestamp' },
+ ], done);
+ });
+ });
+
+ describe('setInitialNotes', () => {
+ it('should set initial notes', (done) => {
+ testAction(actions.setInitialNotes, null, { notes: [] }, [
+ { type: 'SET_INITIAL_NOTES', payload: [individualNote] },
+ ], done);
+ });
+ });
+
+ describe('setTargetNoteHash', () => {
+ it('should set target note hash', (done) => {
+ testAction(actions.setTargetNoteHash, null, { notes: [] }, [
+ { type: 'SET_TARGET_NOTE_HASH', payload: 'hash' },
+ ], done);
+ });
+ });
+
+ describe('toggleDiscussion', () => {
+ it('should toggle discussion', (done) => {
+ testAction(actions.toggleDiscussion, null, { notes: [discussionMock] }, [
+ { type: 'TOGGLE_DISCUSSION', payload: { discussionId: discussionMock.id } },
+ ], done);
+ });
+ });
+});
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
new file mode 100644
index 00000000000..48ee1bf9a52
--- /dev/null
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -0,0 +1,58 @@
+import * as getters from '~/notes/stores/getters';
+import { notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
+
+describe('Getters Notes Store', () => {
+ let state;
+ beforeEach(() => {
+ state = {
+ notes: [individualNote],
+ targetNoteHash: 'hash',
+ lastFetchedAt: 'timestamp',
+
+ notesData: notesDataMock,
+ userData: userDataMock,
+ issueData: issueDataMock,
+ };
+ });
+ describe('notes', () => {
+ it('should return all notes in the store', () => {
+ expect(getters.notes(state)).toEqual([individualNote]);
+ });
+ });
+
+ describe('targetNoteHash', () => {
+ it('should return `targetNoteHash`', () => {
+ expect(getters.targetNoteHash(state)).toEqual('hash');
+ });
+ });
+
+ describe('getNotesData', () => {
+ it('should return all data in `notesData`', () => {
+ expect(getters.getNotesData(state)).toEqual(notesDataMock);
+ });
+ });
+
+ describe('getIssueData', () => {
+ it('should return all data in `issueData`', () => {
+ expect(getters.getIssueData(state)).toEqual(issueDataMock);
+ });
+ });
+
+ describe('getUserData', () => {
+ it('should return all data in `userData`', () => {
+ expect(getters.getUserData(state)).toEqual(userDataMock);
+ });
+ });
+
+ describe('notesById', () => {
+ it('should return the note for the given id', () => {
+ expect(getters.notesById(state)).toEqual({ 1390: individualNote.notes[0] });
+ });
+ });
+
+ describe('getCurrentUserLastNote', () => {
+ it('should return the last note of the current user', () => {
+ expect(getters.getCurrentUserLastNote(state)).toEqual(individualNote.notes[0]);
+ });
+ });
+});
diff --git a/spec/javascripts/notes/stores/helpers.js b/spec/javascripts/notes/stores/helpers.js
new file mode 100644
index 00000000000..2d386fe1da5
--- /dev/null
+++ b/spec/javascripts/notes/stores/helpers.js
@@ -0,0 +1,37 @@
+/* eslint-disable */
+
+/**
+ * helper for testing action with expected mutations
+ * https://vuex.vuejs.org/en/testing.html
+ */
+export default (action, payload, state, expectedMutations, done) => {
+ let count = 0;
+
+ // mock commit
+ const commit = (type, payload) => {
+ const mutation = expectedMutations[count];
+
+ try {
+ expect(mutation.type).to.equal(type);
+ if (payload) {
+ expect(mutation.payload).to.deep.equal(payload);
+ }
+ } catch (error) {
+ done(error);
+ }
+
+ count++;
+ if (count >= expectedMutations.length) {
+ done();
+ }
+ };
+
+ // call the action with mocked store and arguments
+ action({ commit, state }, payload);
+
+ // check if no mutations should have been dispatched
+ if (expectedMutations.length === 0) {
+ expect(count).to.equal(0);
+ done();
+ }
+};
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
new file mode 100644
index 00000000000..a38f29c1e39
--- /dev/null
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -0,0 +1,207 @@
+import mutations from '~/notes/stores/mutations';
+import { note, discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
+
+describe('Mutation Notes Store', () => {
+ describe('ADD_NEW_NOTE', () => {
+ it('should add a new note to an array of notes', () => {
+ const state = { notes: [] };
+ mutations.ADD_NEW_NOTE(state, note);
+
+ expect(state).toEqual({
+ notes: [{
+ expanded: true,
+ id: note.discussion_id,
+ individual_note: true,
+ notes: [note],
+ reply_id: note.discussion_id,
+ }],
+ });
+ });
+ });
+
+ describe('ADD_NEW_REPLY_TO_DISCUSSION', () => {
+ it('should add a reply to a specific discussion', () => {
+ const state = { notes: [discussionMock] };
+ const newReply = Object.assign({}, note, { discussion_id: discussionMock.id });
+ mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply);
+
+ expect(state.notes[0].notes.length).toEqual(4);
+ });
+ });
+
+ describe('DELETE_NOTE', () => {
+ it('should delete a note ', () => {
+ const state = { notes: [discussionMock] };
+ const toDelete = discussionMock.notes[0];
+ const lengthBefore = discussionMock.notes.length;
+
+ mutations.DELETE_NOTE(state, toDelete);
+
+ expect(state.notes[0].notes.length).toEqual(lengthBefore - 1);
+ });
+ });
+
+ describe('REMOVE_PLACEHOLDER_NOTES', () => {
+ it('should remove all placeholder notes in indivudal notes and discussion', () => {
+ const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true });
+ const state = { notes: [placeholderNote] };
+ mutations.REMOVE_PLACEHOLDER_NOTES(state);
+
+ expect(state.notes).toEqual([]);
+ });
+ });
+
+ describe('SET_NOTES_DATA', () => {
+ it('should set an object with notesData', () => {
+ const state = {
+ notesData: {},
+ };
+
+ mutations.SET_NOTES_DATA(state, notesDataMock);
+ expect(state.notesData).toEqual(notesDataMock);
+ });
+ });
+
+ describe('SET_ISSUE_DATA', () => {
+ it('should set the issue data', () => {
+ const state = {
+ issueData: {},
+ };
+
+ mutations.SET_ISSUE_DATA(state, issueDataMock);
+ expect(state.issueData).toEqual(issueDataMock);
+ });
+ });
+
+ describe('SET_USER_DATA', () => {
+ it('should set the user data', () => {
+ const state = {
+ userData: {},
+ };
+
+ mutations.SET_USER_DATA(state, userDataMock);
+ expect(state.userData).toEqual(userDataMock);
+ });
+ });
+
+ describe('SET_INITIAL_NOTES', () => {
+ it('should set the initial notes received', () => {
+ const state = {
+ notes: [],
+ };
+
+ mutations.SET_INITIAL_NOTES(state, [note]);
+ expect(state.notes).toEqual([note]);
+ });
+ });
+
+ describe('SET_LAST_FETCHED_AT', () => {
+ it('should set timestamp', () => {
+ const state = {
+ lastFetchedAt: [],
+ };
+
+ mutations.SET_LAST_FETCHED_AT(state, 'timestamp');
+ expect(state.lastFetchedAt).toEqual('timestamp');
+ });
+ });
+
+ describe('SET_TARGET_NOTE_HASH', () => {
+ it('should set the note hash', () => {
+ const state = {
+ targetNoteHash: [],
+ };
+
+ mutations.SET_TARGET_NOTE_HASH(state, 'hash');
+ expect(state.targetNoteHash).toEqual('hash');
+ });
+ });
+
+ describe('SHOW_PLACEHOLDER_NOTE', () => {
+ it('should set a placeholder note', () => {
+ const state = {
+ notes: [],
+ };
+ mutations.SHOW_PLACEHOLDER_NOTE(state, note);
+ expect(state.notes[0].isPlaceholderNote).toEqual(true);
+ });
+ });
+
+ describe('TOGGLE_AWARD', () => {
+ it('should add award if user has not reacted yet', () => {
+ const state = {
+ notes: [note],
+ userData: userDataMock,
+ };
+
+ const data = {
+ note,
+ awardName: 'cartwheel',
+ };
+
+ mutations.TOGGLE_AWARD(state, data);
+ const lastIndex = state.notes[0].award_emoji.length - 1;
+
+ expect(state.notes[0].award_emoji[lastIndex]).toEqual({
+ name: 'cartwheel',
+ user: { id: userDataMock.id, name: userDataMock.name, username: userDataMock.username },
+ });
+ });
+
+ it('should remove award if user already reacted', () => {
+ const state = {
+ notes: [note],
+ userData: {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ },
+ };
+
+ const data = {
+ note,
+ awardName: 'bath_tone3',
+ };
+ mutations.TOGGLE_AWARD(state, data);
+ expect(state.notes[0].award_emoji.length).toEqual(2);
+ });
+ });
+
+ describe('TOGGLE_DISCUSSION', () => {
+ it('should open a closed discussion', () => {
+ const discussion = Object.assign({}, discussionMock, { expanded: false });
+
+ const state = {
+ notes: [discussion],
+ };
+
+ mutations.TOGGLE_DISCUSSION(state, { discussionId: discussion.id });
+
+ expect(state.notes[0].expanded).toEqual(true);
+ });
+
+ it('should close a opened discussion', () => {
+ const state = {
+ notes: [discussionMock],
+ };
+
+ mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id });
+
+ expect(state.notes[0].expanded).toEqual(false);
+ });
+ });
+
+ describe('UPDATE_NOTE', () => {
+ it('should update a note', () => {
+ const state = {
+ notes: [individualNote],
+ };
+
+ const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' });
+
+ mutations.UPDATE_NOTE(state, updated);
+
+ expect(state.notes[0].notes[0].note).toEqual('Foo');
+ });
+ });
+});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 2c096ed08a8..8c5ad8914b0 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -32,14 +32,14 @@ import '~/notes';
describe('Notes', function() {
const FLASH_TYPE_ALERT = 'alert';
- var commentsTemplate = 'issues/issue_with_comment.html.raw';
+ var commentsTemplate = 'merge_requests/merge_request_with_comment.html.raw';
preloadFixtures(commentsTemplate);
beforeEach(function () {
loadFixtures(commentsTemplate);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
- $('body').data('page', 'projects:issues:show');
+ $('body').data('page', 'projects:merge_requets:show');
});
describe('task lists', function() {
@@ -53,17 +53,19 @@ import '~/notes';
it('modifies the Markdown field', function() {
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
- $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent);
- expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+ $('input[type=checkbox]').attr('checked', true)[1].dispatchEvent(changeEvent);
+
+ expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item');
});
it('submits an ajax request on tasklist:changed', function() {
spyOn(jQuery, 'ajax').and.callFake(function(req) {
expect(req.type).toBe('PATCH');
- expect(req.url).toBe('http://test.host/frontend-fixtures/issues-project/notes/1');
+ expect(req.url).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1.json');
return expect(req.data.note).not.toBe(null);
});
- $('.js-task-list-field').trigger('tasklist:changed');
+
+ $('.js-task-list-field.js-note-text').trigger('tasklist:changed');
});
});
diff --git a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js b/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js
index 6120d224ac0..ed481cb60a1 100644
--- a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js
+++ b/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
-import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_schedules_callout';
+import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_schedules_callout.vue';
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
const cookieKey = 'pipeline_schedules_callout_dismissed';
diff --git a/spec/javascripts/pipelines/navigation_tabs_spec.js b/spec/javascripts/pipelines/navigation_tabs_spec.js
new file mode 100644
index 00000000000..53a88e6322f
--- /dev/null
+++ b/spec/javascripts/pipelines/navigation_tabs_spec.js
@@ -0,0 +1,127 @@
+import Vue from 'vue';
+import navigationTabs from '~/pipelines/components/navigation_tabs.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('navigation tabs pipeline component', () => {
+ let vm;
+ let Component;
+ let data;
+
+ beforeEach(() => {
+ data = {
+ scope: 'all',
+ count: {
+ all: 16,
+ running: 1,
+ pending: 10,
+ finished: 0,
+ },
+ paths: {
+ allPath: '/gitlab-org/gitlab-ce/pipelines',
+ pendingPath: '/gitlab-org/gitlab-ce/pipelines?scope=pending',
+ finishedPath: '/gitlab-org/gitlab-ce/pipelines?scope=finished',
+ runningPath: '/gitlab-org/gitlab-ce/pipelines?scope=running',
+ branchesPath: '/gitlab-org/gitlab-ce/pipelines?scope=branches',
+ tagsPath: '/gitlab-org/gitlab-ce/pipelines?scope=tags',
+ },
+ };
+
+ Component = Vue.extend(navigationTabs);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render tabs with correct paths', () => {
+ vm = mountComponent(Component, data);
+
+ // All
+ const allTab = vm.$el.querySelector('.js-pipelines-tab-all a');
+ expect(allTab.textContent.trim()).toContain('All');
+ expect(allTab.getAttribute('href')).toEqual(data.paths.allPath);
+
+ // Pending
+ const pendingTab = vm.$el.querySelector('.js-pipelines-tab-pending a');
+ expect(pendingTab.textContent.trim()).toContain('Pending');
+ expect(pendingTab.getAttribute('href')).toEqual(data.paths.pendingPath);
+
+ // Running
+ const runningTab = vm.$el.querySelector('.js-pipelines-tab-running a');
+ expect(runningTab.textContent.trim()).toContain('Running');
+ expect(runningTab.getAttribute('href')).toEqual(data.paths.runningPath);
+
+ // Finished
+ const finishedTab = vm.$el.querySelector('.js-pipelines-tab-finished a');
+ expect(finishedTab.textContent.trim()).toContain('Finished');
+ expect(finishedTab.getAttribute('href')).toEqual(data.paths.finishedPath);
+
+ // Branches
+ const branchesTab = vm.$el.querySelector('.js-pipelines-tab-branches a');
+ expect(branchesTab.textContent.trim()).toContain('Branches');
+
+ // Tags
+ const tagsTab = vm.$el.querySelector('.js-pipelines-tab-tags a');
+ expect(tagsTab.textContent.trim()).toContain('Tags');
+ });
+
+ describe('scope', () => {
+ it('should render scope provided as active tab', () => {
+ vm = mountComponent(Component, data);
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').className).toContain('active');
+ });
+ });
+
+ describe('badges', () => {
+ it('should render provided number', () => {
+ vm = mountComponent(Component, data);
+ // All
+ expect(
+ vm.$el.querySelector('.js-totalbuilds-count').textContent.trim(),
+ ).toContain(data.count.all);
+
+ // Pending
+ expect(
+ vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim(),
+ ).toContain(data.count.pending);
+
+ // Running
+ expect(
+ vm.$el.querySelector('.js-pipelines-tab-running .badge').textContent.trim(),
+ ).toContain(data.count.running);
+
+ // Finished
+ expect(
+ vm.$el.querySelector('.js-pipelines-tab-finished .badge').textContent.trim(),
+ ).toContain(data.count.finished);
+ });
+
+ it('should not render badge when number is undefined', () => {
+ vm = mountComponent(Component, {
+ scope: 'all',
+ paths: {},
+ count: {},
+ });
+
+ // All
+ expect(
+ vm.$el.querySelector('.js-totalbuilds-count'),
+ ).toEqual(null);
+
+ // Pending
+ expect(
+ vm.$el.querySelector('.js-pipelines-tab-pending .badge'),
+ ).toEqual(null);
+
+ // Running
+ expect(
+ vm.$el.querySelector('.js-pipelines-tab-running .badge'),
+ ).toEqual(null);
+
+ // Finished
+ expect(
+ vm.$el.querySelector('.js-pipelines-tab-finished .badge'),
+ ).toEqual(null);
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index 3c4b20a5f06..256fdbe743c 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -16,6 +16,7 @@ describe('Pipeline Url Component', () => {
path: 'foo',
flags: {},
},
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
@@ -30,6 +31,7 @@ describe('Pipeline Url Component', () => {
path: 'foo',
flags: {},
},
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
@@ -50,6 +52,7 @@ describe('Pipeline Url Component', () => {
path: '/',
},
},
+ autoDevopsHelpPath: 'foo',
};
const component = new PipelineUrlComponent({
@@ -73,6 +76,7 @@ describe('Pipeline Url Component', () => {
path: 'foo',
flags: {},
},
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
@@ -91,6 +95,7 @@ describe('Pipeline Url Component', () => {
stuck: true,
},
},
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
@@ -98,4 +103,26 @@ describe('Pipeline Url Component', () => {
expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid');
expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck');
});
+
+ it('should render a badge for autodevops', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {
+ latest: true,
+ yaml_errors: true,
+ stuck: true,
+ auto_devops: true,
+ },
+ },
+ autoDevopsHelpPath: 'foo',
+ },
+ }).$mount();
+
+ expect(
+ component.$el.querySelector('.js-pipeline-url-autodevops').textContent.trim(),
+ ).toEqual('Auto DevOps');
+ });
});
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index 7ce39dca112..d7456a48bc1 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -9,7 +9,7 @@ describe('Pipelines Table Row', () => {
el: document.querySelector('.test-dom-element'),
propsData: {
pipeline,
- service: {},
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
};
diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js
index 3afe89c8db4..4f5eb42eb35 100644
--- a/spec/javascripts/pipelines/pipelines_table_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_spec.js
@@ -22,6 +22,7 @@ describe('Pipelines Table', () => {
component = new PipelinesTableComponent({
propsData: {
pipelines: [],
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
});
@@ -47,6 +48,7 @@ describe('Pipelines Table', () => {
const component = new PipelinesTableComponent({
propsData: {
pipelines: [],
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0);
@@ -58,6 +60,7 @@ describe('Pipelines Table', () => {
const component = new PipelinesTableComponent({
propsData: {
pipelines: [pipeline],
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
diff --git a/spec/javascripts/pretty_time_spec.js b/spec/javascripts/pretty_time_spec.js
index de99e7e3894..0a6c479a95b 100644
--- a/spec/javascripts/pretty_time_spec.js
+++ b/spec/javascripts/pretty_time_spec.js
@@ -76,6 +76,87 @@ import '~/lib/utils/pretty_time';
expect(aboveOneWeek.days).toBe(3);
expect(aboveOneWeek.weeks).toBe(173);
});
+
+ it('should correctly accept a custom param for hoursPerDay', function () {
+ const parser = prettyTime.parseSeconds;
+ const config = { hoursPerDay: 24 };
+
+ const aboveOneHour = parser(4800, config);
+
+ expect(aboveOneHour.minutes).toBe(20);
+ expect(aboveOneHour.hours).toBe(1);
+ expect(aboveOneHour.days).toBe(0);
+ expect(aboveOneHour.weeks).toBe(0);
+
+ const aboveOneDay = parser(110000, config);
+
+ expect(aboveOneDay.minutes).toBe(33);
+ expect(aboveOneDay.hours).toBe(6);
+ expect(aboveOneDay.days).toBe(1);
+ expect(aboveOneDay.weeks).toBe(0);
+
+ const aboveOneWeek = parser(25000000, config);
+
+ expect(aboveOneWeek.minutes).toBe(26);
+ expect(aboveOneWeek.hours).toBe(8);
+ expect(aboveOneWeek.days).toBe(4);
+
+ expect(aboveOneWeek.weeks).toBe(57);
+ });
+
+ it('should correctly accept a custom param for daysPerWeek', function () {
+ const parser = prettyTime.parseSeconds;
+ const config = { daysPerWeek: 7 };
+
+ const aboveOneHour = parser(4800, config);
+
+ expect(aboveOneHour.minutes).toBe(20);
+ expect(aboveOneHour.hours).toBe(1);
+ expect(aboveOneHour.days).toBe(0);
+ expect(aboveOneHour.weeks).toBe(0);
+
+ const aboveOneDay = parser(110000, config);
+
+ expect(aboveOneDay.minutes).toBe(33);
+ expect(aboveOneDay.hours).toBe(6);
+ expect(aboveOneDay.days).toBe(3);
+ expect(aboveOneDay.weeks).toBe(0);
+
+ const aboveOneWeek = parser(25000000, config);
+
+ expect(aboveOneWeek.minutes).toBe(26);
+ expect(aboveOneWeek.hours).toBe(0);
+ expect(aboveOneWeek.days).toBe(0);
+
+ expect(aboveOneWeek.weeks).toBe(124);
+ });
+
+ it('should correctly accept custom params for daysPerWeek and hoursPerDay', function () {
+ const parser = prettyTime.parseSeconds;
+ const config = { daysPerWeek: 55, hoursPerDay: 14 };
+
+ const aboveOneHour = parser(4800, config);
+
+ expect(aboveOneHour.minutes).toBe(20);
+ expect(aboveOneHour.hours).toBe(1);
+ expect(aboveOneHour.days).toBe(0);
+ expect(aboveOneHour.weeks).toBe(0);
+
+ const aboveOneDay = parser(110000, config);
+
+ expect(aboveOneDay.minutes).toBe(33);
+ expect(aboveOneDay.hours).toBe(2);
+ expect(aboveOneDay.days).toBe(2);
+ expect(aboveOneDay.weeks).toBe(0);
+
+ const aboveOneWeek = parser(25000000, config);
+
+ expect(aboveOneWeek.minutes).toBe(26);
+ expect(aboveOneWeek.hours).toBe(0);
+ expect(aboveOneWeek.days).toBe(1);
+
+ expect(aboveOneWeek.weeks).toBe(9);
+ });
});
describe('stringifyTime', function () {
diff --git a/spec/javascripts/project_select_combo_button_spec.js b/spec/javascripts/project_select_combo_button_spec.js
index e10a5a3bef6..dda83645c92 100644
--- a/spec/javascripts/project_select_combo_button_spec.js
+++ b/spec/javascripts/project_select_combo_button_spec.js
@@ -32,11 +32,6 @@ describe('Project Select Combo Button', function () {
this.comboButton = new ProjectSelectComboButton(this.projectSelectInput);
});
- it('newItemBtn is disabled', function () {
- expect(this.newItemBtn.hasAttribute('disabled')).toBe(true);
- expect(this.newItemBtn.classList.contains('disabled')).toBe(true);
- });
-
it('newItemBtn href is null', function () {
expect(this.newItemBtn.getAttribute('href')).toBe('');
});
@@ -53,11 +48,6 @@ describe('Project Select Combo Button', function () {
this.comboButton = new ProjectSelectComboButton(this.projectSelectInput);
});
- it('newItemBtn is not disabled', function () {
- expect(this.newItemBtn.hasAttribute('disabled')).toBe(false);
- expect(this.newItemBtn.classList.contains('disabled')).toBe(false);
- });
-
it('newItemBtn href is correctly set', function () {
expect(this.newItemBtn.getAttribute('href')).toBe(this.defaults.projectMeta.url);
});
@@ -82,11 +72,6 @@ describe('Project Select Combo Button', function () {
.trigger('change');
});
- it('newItemBtn is not disabled', function () {
- expect(this.newItemBtn.hasAttribute('disabled')).toBe(false);
- expect(this.newItemBtn.classList.contains('disabled')).toBe(false);
- });
-
it('newItemBtn href is correctly set', function () {
expect(this.newItemBtn.getAttribute('href'))
.toBe('http://myothercoolproject.com/issues/new');
@@ -101,5 +86,40 @@ describe('Project Select Combo Button', function () {
window.localStorage.clear();
});
});
+
+ describe('deriveTextVariants', function () {
+ beforeEach(function () {
+ this.mockExecutionContext = {
+ resourceType: '',
+ resourceLabel: '',
+ };
+
+ this.comboButton = new ProjectSelectComboButton(this.projectSelectInput);
+
+ this.method = this.comboButton.deriveTextVariants.bind(this.mockExecutionContext);
+ });
+
+ it('correctly derives test variants for merge requests', function () {
+ this.mockExecutionContext.resourceType = 'merge_requests';
+ this.mockExecutionContext.resourceLabel = 'New merge request';
+
+ const returnedVariants = this.method();
+
+ expect(returnedVariants.localStorageItemType).toBe('new-merge-request');
+ expect(returnedVariants.defaultTextPrefix).toBe('New merge request');
+ expect(returnedVariants.presetTextSuffix).toBe('merge request');
+ });
+
+ it('correctly derives text variants for issues', function () {
+ this.mockExecutionContext.resourceType = 'issues';
+ this.mockExecutionContext.resourceLabel = 'New issue';
+
+ const returnedVariants = this.method();
+
+ expect(returnedVariants.localStorageItemType).toBe('new-issue');
+ expect(returnedVariants.defaultTextPrefix).toBe('New issue');
+ expect(returnedVariants.presetTextSuffix).toBe('issue');
+ });
+ });
});
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
deleted file mode 100644
index cc336180ff7..00000000000
--- a/spec/javascripts/project_title_spec.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/* global Project */
-
-import 'select2/select2';
-import '~/gl_dropdown';
-import '~/api';
-import '~/project_select';
-import '~/project';
-
-describe('Project Title', () => {
- preloadFixtures('issues/open-issue.html.raw');
- loadJSONFixtures('projects.json');
-
- beforeEach(() => {
- loadFixtures('issues/open-issue.html.raw');
-
- window.gon = {};
- window.gon.api_version = 'v3';
-
- // eslint-disable-next-line no-new
- new Project();
- });
-
- describe('project list', () => {
- let reqUrl;
- let reqData;
-
- beforeEach(() => {
- const fakeResponseData = getJSONFixture('projects.json');
- spyOn(jQuery, 'ajax').and.callFake((req) => {
- const def = $.Deferred();
- reqUrl = req.url;
- reqData = req.data;
- def.resolve(fakeResponseData);
- return def.promise();
- });
- });
-
- it('toggles dropdown', () => {
- const $menu = $('.js-dropdown-menu-projects');
- $('.js-projects-dropdown-toggle').click();
- expect($menu).toHaveClass('open');
- expect(reqUrl).toBe('/api/v3/projects.json?simple=true');
- expect(reqData).toEqual({
- search: '',
- order_by: 'last_activity_at',
- per_page: 20,
- membership: true,
- });
- $menu.find('.dropdown-menu-close-icon').click();
- expect($menu).not.toHaveClass('open');
- });
- });
-
- afterEach(() => {
- window.gon = {};
- });
-});
diff --git a/spec/javascripts/projects_dropdown/components/app_spec.js b/spec/javascripts/projects_dropdown/components/app_spec.js
new file mode 100644
index 00000000000..42f0f6fc1af
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/components/app_spec.js
@@ -0,0 +1,348 @@
+import Vue from 'vue';
+
+import bp from '~/breakpoints';
+import appComponent from '~/projects_dropdown/components/app.vue';
+import eventHub from '~/projects_dropdown/event_hub';
+import ProjectsStore from '~/projects_dropdown/store/projects_store';
+import ProjectsService from '~/projects_dropdown/service/projects_service';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { currentSession, mockProject, mockRawProject } from '../mock_data';
+
+const createComponent = () => {
+ gon.api_version = currentSession.apiVersion;
+ const Component = Vue.extend(appComponent);
+ const store = new ProjectsStore();
+ const service = new ProjectsService(currentSession.username);
+
+ return mountComponent(Component, {
+ store,
+ service,
+ currentUserName: currentSession.username,
+ currentProject: currentSession.project,
+ });
+};
+
+const returnServicePromise = (data, failed) => new Promise((resolve, reject) => {
+ if (failed) {
+ reject(data);
+ } else {
+ resolve({
+ json() {
+ return data;
+ },
+ });
+ }
+});
+
+describe('AppComponent', () => {
+ describe('computed', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('frequentProjects', () => {
+ it('should return list of frequently accessed projects from store', () => {
+ expect(vm.frequentProjects).toBeDefined();
+ expect(vm.frequentProjects.length).toBe(0);
+
+ vm.store.setFrequentProjects([mockProject]);
+ expect(vm.frequentProjects).toBeDefined();
+ expect(vm.frequentProjects.length).toBe(1);
+ });
+ });
+
+ describe('searchProjects', () => {
+ it('should return list of frequently accessed projects from store', () => {
+ expect(vm.searchProjects).toBeDefined();
+ expect(vm.searchProjects.length).toBe(0);
+
+ vm.store.setSearchedProjects([mockRawProject]);
+ expect(vm.searchProjects).toBeDefined();
+ expect(vm.searchProjects.length).toBe(1);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('toggleFrequentProjectsList', () => {
+ it('should toggle props which control visibility of Frequent Projects list from state passed', () => {
+ vm.toggleFrequentProjectsList(true);
+ expect(vm.isLoadingProjects).toBeFalsy();
+ expect(vm.isSearchListVisible).toBeFalsy();
+ expect(vm.isFrequentsListVisible).toBeTruthy();
+
+ vm.toggleFrequentProjectsList(false);
+ expect(vm.isLoadingProjects).toBeTruthy();
+ expect(vm.isSearchListVisible).toBeTruthy();
+ expect(vm.isFrequentsListVisible).toBeFalsy();
+ });
+ });
+
+ describe('toggleSearchProjectsList', () => {
+ it('should toggle props which control visibility of Searched Projects list from state passed', () => {
+ vm.toggleSearchProjectsList(true);
+ expect(vm.isLoadingProjects).toBeFalsy();
+ expect(vm.isFrequentsListVisible).toBeFalsy();
+ expect(vm.isSearchListVisible).toBeTruthy();
+
+ vm.toggleSearchProjectsList(false);
+ expect(vm.isLoadingProjects).toBeTruthy();
+ expect(vm.isFrequentsListVisible).toBeTruthy();
+ expect(vm.isSearchListVisible).toBeFalsy();
+ });
+ });
+
+ describe('toggleLoader', () => {
+ it('should toggle props which control visibility of list loading animation from state passed', () => {
+ vm.toggleLoader(true);
+ expect(vm.isFrequentsListVisible).toBeFalsy();
+ expect(vm.isSearchListVisible).toBeFalsy();
+ expect(vm.isLoadingProjects).toBeTruthy();
+
+ vm.toggleLoader(false);
+ expect(vm.isFrequentsListVisible).toBeTruthy();
+ expect(vm.isSearchListVisible).toBeTruthy();
+ expect(vm.isLoadingProjects).toBeFalsy();
+ });
+ });
+
+ describe('fetchFrequentProjects', () => {
+ it('should set props for loading animation to `true` while frequent projects list is being loaded', () => {
+ spyOn(vm, 'toggleLoader');
+
+ vm.fetchFrequentProjects();
+ expect(vm.isLocalStorageFailed).toBeFalsy();
+ expect(vm.toggleLoader).toHaveBeenCalledWith(true);
+ });
+
+ it('should set props for loading animation to `false` and props for frequent projects list to `true` once data is loaded', () => {
+ const mockData = [mockProject];
+
+ spyOn(vm.service, 'getFrequentProjects').and.returnValue(mockData);
+ spyOn(vm.store, 'setFrequentProjects');
+ spyOn(vm, 'toggleFrequentProjectsList');
+
+ vm.fetchFrequentProjects();
+ expect(vm.service.getFrequentProjects).toHaveBeenCalled();
+ expect(vm.store.setFrequentProjects).toHaveBeenCalledWith(mockData);
+ expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true);
+ });
+
+ it('should set props for failure message to `true` when method fails to fetch frequent projects list', () => {
+ spyOn(vm.service, 'getFrequentProjects').and.returnValue(null);
+ spyOn(vm.store, 'setFrequentProjects');
+ spyOn(vm, 'toggleFrequentProjectsList');
+
+ expect(vm.isLocalStorageFailed).toBeFalsy();
+
+ vm.fetchFrequentProjects();
+ expect(vm.service.getFrequentProjects).toHaveBeenCalled();
+ expect(vm.store.setFrequentProjects).toHaveBeenCalledWith([]);
+ expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true);
+ expect(vm.isLocalStorageFailed).toBeTruthy();
+ });
+
+ it('should set props for search results list to `true` if search query was already made previously', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('md');
+ spyOn(vm.service, 'getFrequentProjects');
+ spyOn(vm, 'toggleSearchProjectsList');
+
+ vm.searchQuery = 'test';
+ vm.fetchFrequentProjects();
+ expect(vm.service.getFrequentProjects).not.toHaveBeenCalled();
+ expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
+ });
+
+ it('should set props for frequent projects list to `true` if search query was already made but screen size is less than 768px', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
+ spyOn(vm, 'toggleSearchProjectsList');
+ spyOn(vm.service, 'getFrequentProjects');
+
+ vm.searchQuery = 'test';
+ vm.fetchFrequentProjects();
+ expect(vm.service.getFrequentProjects).toHaveBeenCalled();
+ expect(vm.toggleSearchProjectsList).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('fetchSearchedProjects', () => {
+ const searchQuery = 'test';
+
+ it('should perform search with provided search query', (done) => {
+ const mockData = [mockRawProject];
+ spyOn(vm, 'toggleLoader');
+ spyOn(vm, 'toggleSearchProjectsList');
+ spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise(mockData));
+ spyOn(vm.store, 'setSearchedProjects');
+
+ vm.fetchSearchedProjects(searchQuery);
+ setTimeout(() => {
+ expect(vm.searchQuery).toBe(searchQuery);
+ expect(vm.toggleLoader).toHaveBeenCalledWith(true);
+ expect(vm.service.getSearchedProjects).toHaveBeenCalledWith(searchQuery);
+ expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
+ expect(vm.store.setSearchedProjects).toHaveBeenCalledWith(mockData);
+ done();
+ }, 0);
+ });
+
+ it('should update props for showing search failure', (done) => {
+ spyOn(vm, 'toggleSearchProjectsList');
+ spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise({}, true));
+
+ vm.fetchSearchedProjects(searchQuery);
+ setTimeout(() => {
+ expect(vm.searchQuery).toBe(searchQuery);
+ expect(vm.service.getSearchedProjects).toHaveBeenCalledWith(searchQuery);
+ expect(vm.isSearchFailed).toBeTruthy();
+ expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
+ done();
+ }, 0);
+ });
+ });
+
+ describe('logCurrentProjectAccess', () => {
+ it('should log current project access via service', (done) => {
+ spyOn(vm.service, 'logProjectAccess');
+
+ vm.currentProject = mockProject;
+ vm.logCurrentProjectAccess();
+
+ setTimeout(() => {
+ expect(vm.service.logProjectAccess).toHaveBeenCalledWith(mockProject);
+ done();
+ }, 1);
+ });
+ });
+
+ describe('handleSearchClear', () => {
+ it('should show frequent projects list when search input is cleared', () => {
+ spyOn(vm.store, 'clearSearchedProjects');
+ spyOn(vm, 'toggleFrequentProjectsList');
+
+ vm.handleSearchClear();
+
+ expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true);
+ expect(vm.store.clearSearchedProjects).toHaveBeenCalled();
+ expect(vm.searchQuery).toBe('');
+ });
+ });
+
+ describe('handleSearchFailure', () => {
+ it('should show failure message within dropdown', () => {
+ spyOn(vm, 'toggleSearchProjectsList');
+
+ vm.handleSearchFailure();
+ expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
+ expect(vm.isSearchFailed).toBeTruthy();
+ });
+ });
+ });
+
+ describe('created', () => {
+ it('should bind event listeners on eventHub', (done) => {
+ spyOn(eventHub, '$on');
+
+ createComponent().$mount();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$on).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('searchProjects', jasmine.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('searchCleared', jasmine.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('searchFailed', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('should unbind event listeners on eventHub', (done) => {
+ const vm = createComponent();
+ spyOn(eventHub, '$off');
+
+ vm.$mount();
+ vm.$destroy();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$off).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('searchProjects', jasmine.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('searchCleared', jasmine.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('searchFailed', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render search input', () => {
+ expect(vm.$el.querySelector('.search-input-container')).toBeDefined();
+ });
+
+ it('should render loading animation', (done) => {
+ vm.toggleLoader(true);
+ Vue.nextTick(() => {
+ const loadingEl = vm.$el.querySelector('.loading-animation');
+
+ expect(loadingEl).toBeDefined();
+ expect(loadingEl.classList.contains('prepend-top-20')).toBeTruthy();
+ expect(loadingEl.querySelector('i').getAttribute('aria-label')).toBe('Loading projects');
+ done();
+ });
+ });
+
+ it('should render frequent projects list header', (done) => {
+ vm.toggleFrequentProjectsList(true);
+ Vue.nextTick(() => {
+ const sectionHeaderEl = vm.$el.querySelector('.section-header');
+
+ expect(sectionHeaderEl).toBeDefined();
+ expect(sectionHeaderEl.innerText.trim()).toBe('Frequently visited');
+ done();
+ });
+ });
+
+ it('should render frequent projects list', (done) => {
+ vm.toggleFrequentProjectsList(true);
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.projects-list-frequent-container')).toBeDefined();
+ done();
+ });
+ });
+
+ it('should render searched projects list', (done) => {
+ vm.toggleSearchProjectsList(true);
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.section-header')).toBe(null);
+ expect(vm.$el.querySelector('.projects-list-search-container')).toBeDefined();
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js
new file mode 100644
index 00000000000..fcd0f6a3630
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js
@@ -0,0 +1,72 @@
+import Vue from 'vue';
+
+import projectsListFrequentComponent from '~/projects_dropdown/components/projects_list_frequent.vue';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { mockFrequents } from '../mock_data';
+
+const createComponent = () => {
+ const Component = Vue.extend(projectsListFrequentComponent);
+
+ return mountComponent(Component, {
+ projects: mockFrequents,
+ localStorageFailed: false,
+ });
+};
+
+describe('ProjectsListFrequentComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('isListEmpty', () => {
+ it('should return `true` or `false` representing whether if `projects` is empty of not', () => {
+ vm.projects = [];
+ expect(vm.isListEmpty).toBeTruthy();
+
+ vm.projects = mockFrequents;
+ expect(vm.isListEmpty).toBeFalsy();
+ });
+ });
+
+ describe('listEmptyMessage', () => {
+ it('should return appropriate empty list message based on value of `localStorageFailed` prop', () => {
+ vm.localStorageFailed = true;
+ expect(vm.listEmptyMessage).toBe('This feature requires browser localStorage support');
+
+ vm.localStorageFailed = false;
+ expect(vm.listEmptyMessage).toBe('Projects you visit often will appear here');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component element with list of projects', (done) => {
+ vm.projects = mockFrequents;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.classList.contains('projects-list-frequent-container')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(5);
+ done();
+ });
+ });
+
+ it('should render component element with empty message', (done) => {
+ vm.projects = [];
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
new file mode 100644
index 00000000000..171629fcd6b
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
@@ -0,0 +1,65 @@
+import Vue from 'vue';
+
+import projectsListItemComponent from '~/projects_dropdown/components/projects_list_item.vue';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { mockProject } from '../mock_data';
+
+const createComponent = () => {
+ const Component = Vue.extend(projectsListItemComponent);
+
+ return mountComponent(Component, {
+ projectId: mockProject.id,
+ projectName: mockProject.name,
+ namespace: mockProject.namespace,
+ webUrl: mockProject.webUrl,
+ avatarUrl: mockProject.avatarUrl,
+ });
+};
+
+describe('ProjectsListItemComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('hasAvatar', () => {
+ it('should return `true` or `false` if whether avatar is present or not', () => {
+ vm.avatarUrl = 'path/to/avatar.png';
+ expect(vm.hasAvatar).toBeTruthy();
+
+ vm.avatarUrl = null;
+ expect(vm.hasAvatar).toBeFalsy();
+ });
+ });
+
+ describe('highlightedProjectName', () => {
+ it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => {
+ vm.matcher = 'lab';
+ expect(vm.highlightedProjectName).toContain('<b>Lab</b>');
+ });
+
+ it('should return project name as it is if `matcher` is not available', () => {
+ vm.matcher = null;
+ expect(vm.highlightedProjectName).toBe(mockProject.name);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component element', () => {
+ expect(vm.$el.classList.contains('projects-list-item-container')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('a').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.project-item-avatar-container').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.project-item-metadata-container').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.project-title').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.project-namespace').length).toBe(1);
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
new file mode 100644
index 00000000000..67f8a8946c2
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+
+import projectsListSearchComponent from '~/projects_dropdown/components/projects_list_search.vue';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { mockProject } from '../mock_data';
+
+const createComponent = () => {
+ const Component = Vue.extend(projectsListSearchComponent);
+
+ return mountComponent(Component, {
+ projects: [mockProject],
+ matcher: 'lab',
+ searchFailed: false,
+ });
+};
+
+describe('ProjectsListSearchComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('isListEmpty', () => {
+ it('should return `true` or `false` representing whether if `projects` is empty of not', () => {
+ vm.projects = [];
+ expect(vm.isListEmpty).toBeTruthy();
+
+ vm.projects = [mockProject];
+ expect(vm.isListEmpty).toBeFalsy();
+ });
+ });
+
+ describe('listEmptyMessage', () => {
+ it('should return appropriate empty list message based on value of `searchFailed` prop', () => {
+ vm.searchFailed = true;
+ expect(vm.listEmptyMessage).toBe('Something went wrong on our end.');
+
+ vm.searchFailed = false;
+ expect(vm.listEmptyMessage).toBe('Sorry, no projects matched your search');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component element with list of projects', (done) => {
+ vm.projects = [mockProject];
+
+ Vue.nextTick(() => {
+ expect(vm.$el.classList.contains('projects-list-search-container')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(1);
+ done();
+ });
+ });
+
+ it('should render component element with empty message', (done) => {
+ vm.projects = [];
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0);
+ done();
+ });
+ });
+
+ it('should render component element with failure message', (done) => {
+ vm.searchFailed = true;
+ vm.projects = [];
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('li.section-empty.section-failure').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/components/search_spec.js b/spec/javascripts/projects_dropdown/components/search_spec.js
new file mode 100644
index 00000000000..24d8a00b254
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/components/search_spec.js
@@ -0,0 +1,101 @@
+import Vue from 'vue';
+
+import searchComponent from '~/projects_dropdown/components/search.vue';
+import eventHub from '~/projects_dropdown/event_hub';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const createComponent = () => {
+ const Component = Vue.extend(searchComponent);
+
+ return mountComponent(Component);
+};
+
+describe('SearchComponent', () => {
+ describe('methods', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('setFocus', () => {
+ it('should set focus to search input', () => {
+ spyOn(vm.$refs.search, 'focus');
+
+ vm.setFocus();
+ expect(vm.$refs.search.focus).toHaveBeenCalled();
+ });
+ });
+
+ describe('emitSearchEvents', () => {
+ it('should emit `searchProjects` event via eventHub when `searchQuery` present', () => {
+ const searchQuery = 'test';
+ spyOn(eventHub, '$emit');
+ vm.searchQuery = searchQuery;
+ vm.emitSearchEvents();
+ expect(eventHub.$emit).toHaveBeenCalledWith('searchProjects', searchQuery);
+ });
+
+ it('should emit `searchCleared` event via eventHub when `searchQuery` is cleared', () => {
+ spyOn(eventHub, '$emit');
+ vm.searchQuery = '';
+ vm.emitSearchEvents();
+ expect(eventHub.$emit).toHaveBeenCalledWith('searchCleared');
+ });
+ });
+ });
+
+ describe('mounted', () => {
+ it('should listen `dropdownOpen` event', (done) => {
+ spyOn(eventHub, '$on');
+ createComponent();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$on).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('should unbind event listeners on eventHub', (done) => {
+ const vm = createComponent();
+ spyOn(eventHub, '$off');
+
+ vm.$mount();
+ vm.$destroy();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$off).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render component element', () => {
+ const inputEl = vm.$el.querySelector('input.form-control');
+
+ expect(vm.$el.classList.contains('search-input-container')).toBeTruthy();
+ expect(vm.$el.classList.contains('hidden-xs')).toBeTruthy();
+ expect(inputEl).not.toBe(null);
+ expect(inputEl.getAttribute('placeholder')).toBe('Search your projects');
+ expect(vm.$el.querySelector('.search-icon')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/mock_data.js b/spec/javascripts/projects_dropdown/mock_data.js
new file mode 100644
index 00000000000..d6a79fb8ac1
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/mock_data.js
@@ -0,0 +1,96 @@
+export const currentSession = {
+ username: 'root',
+ storageKey: 'root/frequent-projects',
+ apiVersion: 'v4',
+ project: {
+ id: 1,
+ name: 'dummy-project',
+ namespace: 'SamepleGroup / Dummy-Project',
+ webUrl: 'http://127.0.0.1/samplegroup/dummy-project',
+ avatarUrl: null,
+ lastAccessedOn: Date.now(),
+ },
+};
+
+export const mockProject = {
+ id: 1,
+ name: 'GitLab Community Edition',
+ namespace: 'gitlab-org / gitlab-ce',
+ webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce',
+ avatarUrl: null,
+};
+
+export const mockRawProject = {
+ id: 1,
+ name: 'GitLab Community Edition',
+ name_with_namespace: 'gitlab-org / gitlab-ce',
+ web_url: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce',
+ avatar_url: null,
+};
+
+export const mockFrequents = [
+ {
+ id: 1,
+ name: 'GitLab Community Edition',
+ namespace: 'gitlab-org / gitlab-ce',
+ webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce',
+ avatarUrl: null,
+ },
+ {
+ id: 2,
+ name: 'GitLab CI',
+ namespace: 'gitlab-org / gitlab-ci',
+ webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ci',
+ avatarUrl: null,
+ },
+ {
+ id: 3,
+ name: 'Typeahead.Js',
+ namespace: 'twitter / typeahead-js',
+ webUrl: 'http://127.0.0.1:3000/twitter/typeahead-js',
+ avatarUrl: '/uploads/-/system/project/avatar/7/TWBS.png',
+ },
+ {
+ id: 4,
+ name: 'Intel',
+ namespace: 'platform / hardware / bsp / intel',
+ webUrl: 'http://127.0.0.1:3000/platform/hardware/bsp/intel',
+ avatarUrl: null,
+ },
+ {
+ id: 5,
+ name: 'v4.4',
+ namespace: 'platform / hardware / bsp / kernel / common / v4.4',
+ webUrl: 'http://localhost:3000/platform/hardware/bsp/kernel/common/v4.4',
+ avatarUrl: null,
+ },
+];
+
+export const unsortedFrequents = [
+ { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
+ { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
+ { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
+ { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
+ { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
+ { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
+ { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
+ { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
+ { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
+];
+
+/**
+ * This const has a specific order which tests authenticity
+ * of `ProjectsService.getTopFrequentProjects` method so
+ * DO NOT change order of items in this const.
+ */
+export const sortedFrequents = [
+ { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
+ { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
+ { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
+ { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
+ { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
+ { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
+ { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
+ { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
+ { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
+];
diff --git a/spec/javascripts/projects_dropdown/service/projects_service_spec.js b/spec/javascripts/projects_dropdown/service/projects_service_spec.js
new file mode 100644
index 00000000000..d5dd8b3449a
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/service/projects_service_spec.js
@@ -0,0 +1,179 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+import bp from '~/breakpoints';
+import ProjectsService from '~/projects_dropdown/service/projects_service';
+import { FREQUENT_PROJECTS } from '~/projects_dropdown/constants';
+import { currentSession, unsortedFrequents, sortedFrequents } from '../mock_data';
+
+Vue.use(VueResource);
+
+FREQUENT_PROJECTS.MAX_COUNT = 3;
+
+describe('ProjectsService', () => {
+ let service;
+
+ beforeEach(() => {
+ gon.api_version = currentSession.apiVersion;
+ gon.current_user_id = 1;
+ service = new ProjectsService(currentSession.username);
+ });
+
+ describe('contructor', () => {
+ it('should initialize default properties of class', () => {
+ expect(service.isLocalStorageAvailable).toBeTruthy();
+ expect(service.currentUserName).toBe(currentSession.username);
+ expect(service.storageKey).toBe(currentSession.storageKey);
+ expect(service.projectsPath).toBeDefined();
+ });
+ });
+
+ describe('getSearchedProjects', () => {
+ it('should return promise from VueResource HTTP GET', () => {
+ spyOn(service.projectsPath, 'get').and.stub();
+
+ const searchQuery = 'lab';
+ const queryParams = {
+ simple: false,
+ per_page: 20,
+ membership: true,
+ order_by: 'last_activity_at',
+ search: searchQuery,
+ };
+
+ service.getSearchedProjects(searchQuery);
+ expect(service.projectsPath.get).toHaveBeenCalledWith(queryParams);
+ });
+ });
+
+ describe('logProjectAccess', () => {
+ let storage;
+
+ beforeEach(() => {
+ storage = {};
+
+ spyOn(window.localStorage, 'setItem').and.callFake((storageKey, value) => {
+ storage[storageKey] = value;
+ });
+
+ spyOn(window.localStorage, 'getItem').and.callFake((storageKey) => {
+ if (storage[storageKey]) {
+ return storage[storageKey];
+ }
+
+ return null;
+ });
+ });
+
+ it('should create a project store if it does not exist and adds a project', () => {
+ service.logProjectAccess(currentSession.project);
+
+ const projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects.length).toBe(1);
+ expect(projects[0].frequency).toBe(1);
+ expect(projects[0].lastAccessedOn).toBeDefined();
+ });
+
+ it('should prevent inserting same report multiple times into store', () => {
+ service.logProjectAccess(currentSession.project);
+ service.logProjectAccess(currentSession.project);
+
+ const projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects.length).toBe(1);
+ });
+
+ it('should increase frequency of report if it was logged multiple times over the course of an hour', () => {
+ let projects;
+ spyOn(Math, 'abs').and.returnValue(3600001); // this will lead to `diff` > 1;
+ service.logProjectAccess(currentSession.project);
+
+ projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects[0].frequency).toBe(1);
+
+ service.logProjectAccess(currentSession.project);
+ projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects[0].frequency).toBe(2);
+ expect(projects[0].lastAccessedOn).not.toBe(currentSession.project.lastAccessedOn);
+ });
+
+ it('should always update project metadata', () => {
+ let projects;
+ const oldProject = {
+ ...currentSession.project,
+ };
+
+ const newProject = {
+ ...currentSession.project,
+ name: 'New Name',
+ avatarUrl: 'new/avatar.png',
+ namespace: 'New / Namespace',
+ webUrl: 'http://localhost/new/web/url',
+ };
+
+ service.logProjectAccess(oldProject);
+ projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects[0].name).toBe(oldProject.name);
+ expect(projects[0].avatarUrl).toBe(oldProject.avatarUrl);
+ expect(projects[0].namespace).toBe(oldProject.namespace);
+ expect(projects[0].webUrl).toBe(oldProject.webUrl);
+
+ service.logProjectAccess(newProject);
+ projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects[0].name).toBe(newProject.name);
+ expect(projects[0].avatarUrl).toBe(newProject.avatarUrl);
+ expect(projects[0].namespace).toBe(newProject.namespace);
+ expect(projects[0].webUrl).toBe(newProject.webUrl);
+ });
+
+ it('should not add more than 20 projects in store', () => {
+ for (let i = 1; i <= 5; i += 1) {
+ const project = Object.assign(currentSession.project, { id: i });
+ service.logProjectAccess(project);
+ }
+
+ const projects = JSON.parse(storage[currentSession.storageKey]);
+ expect(projects.length).toBe(3);
+ });
+ });
+
+ describe('getTopFrequentProjects', () => {
+ let storage = {};
+
+ beforeEach(() => {
+ storage[currentSession.storageKey] = JSON.stringify(unsortedFrequents);
+
+ spyOn(window.localStorage, 'getItem').and.callFake((storageKey) => {
+ if (storage[storageKey]) {
+ return storage[storageKey];
+ }
+
+ return null;
+ });
+ });
+
+ it('should return top 5 frequently accessed projects for desktop screens', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('md');
+ const frequentProjects = service.getTopFrequentProjects();
+
+ expect(frequentProjects.length).toBe(5);
+ frequentProjects.forEach((project, index) => {
+ expect(project.id).toBe(sortedFrequents[index].id);
+ });
+ });
+
+ it('should return top 3 frequently accessed projects for mobile screens', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
+ const frequentProjects = service.getTopFrequentProjects();
+
+ expect(frequentProjects.length).toBe(3);
+ frequentProjects.forEach((project, index) => {
+ expect(project.id).toBe(sortedFrequents[index].id);
+ });
+ });
+
+ it('should return empty array if there are no projects available in store', () => {
+ storage = {};
+ expect(service.getTopFrequentProjects().length).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/store/projects_store_spec.js b/spec/javascripts/projects_dropdown/store/projects_store_spec.js
new file mode 100644
index 00000000000..e57399d37cd
--- /dev/null
+++ b/spec/javascripts/projects_dropdown/store/projects_store_spec.js
@@ -0,0 +1,41 @@
+import ProjectsStore from '~/projects_dropdown/store/projects_store';
+import { mockProject, mockRawProject } from '../mock_data';
+
+describe('ProjectsStore', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new ProjectsStore();
+ });
+
+ describe('setFrequentProjects', () => {
+ it('should set frequent projects list to state', () => {
+ store.setFrequentProjects([mockProject]);
+
+ expect(store.getFrequentProjects().length).toBe(1);
+ expect(store.getFrequentProjects()[0].id).toBe(mockProject.id);
+ });
+ });
+
+ describe('setSearchedProjects', () => {
+ it('should set searched projects list to state', () => {
+ store.setSearchedProjects([mockRawProject]);
+
+ const processedProjects = store.getSearchedProjects();
+ expect(processedProjects.length).toBe(1);
+ expect(processedProjects[0].id).toBe(mockRawProject.id);
+ expect(processedProjects[0].namespace).toBe(mockRawProject.name_with_namespace);
+ expect(processedProjects[0].webUrl).toBe(mockRawProject.web_url);
+ expect(processedProjects[0].avatarUrl).toBe(mockRawProject.avatar_url);
+ });
+ });
+
+ describe('clearSearchedProjects', () => {
+ it('should clear searched projects list from state', () => {
+ store.setSearchedProjects([mockRawProject]);
+ expect(store.getSearchedProjects().length).toBe(1);
+ store.clearSearchedProjects();
+ expect(store.getSearchedProjects().length).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js
index db2b7d51626..e604dcc152d 100644
--- a/spec/javascripts/repo/components/repo_commit_section_spec.js
+++ b/spec/javascripts/repo/components/repo_commit_section_spec.js
@@ -1,57 +1,57 @@
import Vue from 'vue';
import repoCommitSection from '~/repo/components/repo_commit_section.vue';
import RepoStore from '~/repo/stores/repo_store';
-import RepoHelper from '~/repo/helpers/repo_helper';
-import Api from '~/api';
+import RepoService from '~/repo/services/repo_service';
describe('RepoCommitSection', () => {
const branch = 'master';
const projectUrl = 'projectUrl';
- const openedFiles = [{
+ const changedFiles = [{
id: 0,
changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`,
+ path: 'dir/file0.ext',
newContent: 'a',
}, {
id: 1,
changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`,
+ path: 'dir/file1.ext',
newContent: 'b',
- }, {
+ }];
+ const openedFiles = changedFiles.concat([{
id: 2,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`,
+ path: 'dir/file2.ext',
changed: false,
- }];
+ }]);
RepoStore.projectUrl = projectUrl;
- function createComponent() {
+ function createComponent(el) {
const RepoCommitSection = Vue.extend(repoCommitSection);
- return new RepoCommitSection().$mount();
+ return new RepoCommitSection().$mount(el);
}
it('renders a commit section', () => {
RepoStore.isCommitable = true;
+ RepoStore.currentBranch = branch;
RepoStore.targetBranch = branch;
RepoStore.openedFiles = openedFiles;
- spyOn(RepoHelper, 'getBranch').and.returnValue(branch);
-
const vm = createComponent();
- const changedFiles = [...vm.$el.querySelectorAll('.changed-files > li')];
+ const changedFileElements = [...vm.$el.querySelectorAll('.changed-files > li')];
const commitMessage = vm.$el.querySelector('#commit-message');
- const submitCommit = vm.$el.querySelector('.submit-commit');
+ const submitCommit = vm.$refs.submitCommit;
const targetBranch = vm.$el.querySelector('.target-branch');
expect(vm.$el.querySelector(':scope > form')).toBeTruthy();
- expect(vm.$el.querySelector('.staged-files').textContent).toEqual('Staged files (2)');
- expect(changedFiles.length).toEqual(2);
+ expect(vm.$el.querySelector('.staged-files').textContent.trim()).toEqual('Staged files (2)');
+ expect(changedFileElements.length).toEqual(2);
- changedFiles.forEach((changedFile, i) => {
- const filePath = RepoHelper.getFilePathFromFullPath(openedFiles[i].url, branch);
-
- expect(changedFile.textContent).toEqual(filePath);
+ changedFileElements.forEach((changedFile, i) => {
+ expect(changedFile.textContent.trim()).toEqual(changedFiles[i].path);
});
expect(commitMessage.tagName).toEqual('TEXTAREA');
@@ -59,9 +59,9 @@ describe('RepoCommitSection', () => {
expect(submitCommit.type).toEqual('submit');
expect(submitCommit.disabled).toBeTruthy();
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeFalsy();
- expect(vm.$el.querySelector('.commit-summary').textContent).toEqual('Commit 2 files');
- expect(targetBranch.querySelector(':scope > label').textContent).toEqual('Target branch');
- expect(targetBranch.querySelector('.help-block').textContent).toEqual(branch);
+ expect(vm.$el.querySelector('.commit-summary').textContent.trim()).toEqual('Commit 2 files');
+ expect(targetBranch.querySelector(':scope > label').textContent.trim()).toEqual('Target branch');
+ expect(targetBranch.querySelector('.help-block').textContent.trim()).toEqual(branch);
});
it('does not render if not isCommitable', () => {
@@ -89,14 +89,20 @@ describe('RepoCommitSection', () => {
const projectId = 'projectId';
const commitMessage = 'commitMessage';
RepoStore.isCommitable = true;
+ RepoStore.currentBranch = branch;
+ RepoStore.targetBranch = branch;
RepoStore.openedFiles = openedFiles;
RepoStore.projectId = projectId;
- spyOn(RepoHelper, 'getBranch').and.returnValue(branch);
+ // We need to append to body to get form `submit` events working
+ // Otherwise we run into, "Form submission canceled because the form is not connected"
+ // See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
+ const el = document.createElement('div');
+ document.body.appendChild(el);
- const vm = createComponent();
+ const vm = createComponent(el);
const commitMessageEl = vm.$el.querySelector('#commit-message');
- const submitCommit = vm.$el.querySelector('.submit-commit');
+ const submitCommit = vm.$refs.submitCommit;
vm.commitMessage = commitMessage;
@@ -105,7 +111,7 @@ describe('RepoCommitSection', () => {
expect(submitCommit.disabled).toBeFalsy();
spyOn(vm, 'makeCommit').and.callThrough();
- spyOn(Api, 'commitMultiple');
+ spyOn(RepoService, 'commitFiles').and.callFake(() => Promise.resolve());
submitCommit.click();
@@ -113,10 +119,9 @@ describe('RepoCommitSection', () => {
expect(vm.makeCommit).toHaveBeenCalled();
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeTruthy();
- const args = Api.commitMultiple.calls.allArgs()[0];
- const { commit_message, actions, branch: payloadBranch } = args[1];
+ const args = RepoService.commitFiles.calls.allArgs()[0];
+ const { commit_message, actions, branch: payloadBranch } = args[0];
- expect(args[0]).toBe(projectId);
expect(commit_message).toBe(commitMessage);
expect(actions.length).toEqual(2);
expect(payloadBranch).toEqual(branch);
@@ -124,10 +129,8 @@ describe('RepoCommitSection', () => {
expect(actions[1].action).toEqual('update');
expect(actions[0].content).toEqual(openedFiles[0].newContent);
expect(actions[1].content).toEqual(openedFiles[1].newContent);
- expect(actions[0].file_path)
- .toEqual(RepoHelper.getFilePathFromFullPath(openedFiles[0].url, branch));
- expect(actions[1].file_path)
- .toEqual(RepoHelper.getFilePathFromFullPath(openedFiles[1].url, branch));
+ expect(actions[0].file_path).toEqual(openedFiles[0].path);
+ expect(actions[1].file_path).toEqual(openedFiles[1].path);
done();
});
@@ -140,7 +143,6 @@ describe('RepoCommitSection', () => {
const vm = {
submitCommitsLoading: true,
changedFiles: new Array(10),
- openedFiles: new Array(10),
commitMessage: 'commitMessage',
editMode: true,
};
@@ -149,7 +151,6 @@ describe('RepoCommitSection', () => {
expect(vm.submitCommitsLoading).toEqual(false);
expect(vm.changedFiles).toEqual([]);
- expect(vm.openedFiles).toEqual([]);
expect(vm.commitMessage).toEqual('');
expect(vm.editMode).toEqual(false);
});
diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js
index df2f9697acc..29dc2d21e4b 100644
--- a/spec/javascripts/repo/components/repo_edit_button_spec.js
+++ b/spec/javascripts/repo/components/repo_edit_button_spec.js
@@ -12,18 +12,22 @@ describe('RepoEditButton', () => {
it('renders an edit button that toggles the view state', (done) => {
RepoStore.isCommitable = true;
RepoStore.changedFiles = [];
+ RepoStore.binary = false;
+ RepoStore.openedFiles = [{}, {}];
const vm = createComponent();
expect(vm.$el.tagName).toEqual('BUTTON');
expect(vm.$el.textContent).toMatch('Edit');
- spyOn(vm, 'editClicked').and.callThrough();
+ spyOn(vm, 'editCancelClicked').and.callThrough();
+ spyOn(vm, 'toggleProjectRefsForm');
vm.$el.click();
Vue.nextTick(() => {
- expect(vm.editClicked).toHaveBeenCalled();
+ expect(vm.editCancelClicked).toHaveBeenCalled();
+ expect(vm.toggleProjectRefsForm).toHaveBeenCalled();
expect(vm.$el.textContent).toMatch('Cancel edit');
done();
});
@@ -38,14 +42,10 @@ describe('RepoEditButton', () => {
});
describe('methods', () => {
- describe('editClicked', () => {
- it('sets dialog to open when there are changedFiles', () => {
+ describe('editCancelClicked', () => {
+ it('sets dialog to open when there are changedFiles');
- });
-
- it('toggles editMode and calls toggleBlobView', () => {
-
- });
+ it('toggles editMode and calls toggleBlobView');
});
});
});
diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js
index 35e0c995163..85d55d171f9 100644
--- a/spec/javascripts/repo/components/repo_editor_spec.js
+++ b/spec/javascripts/repo/components/repo_editor_spec.js
@@ -1,26 +1,49 @@
import Vue from 'vue';
import repoEditor from '~/repo/components/repo_editor.vue';
-import RepoStore from '~/repo/stores/repo_store';
describe('RepoEditor', () => {
- function createComponent() {
+ beforeEach(() => {
const RepoEditor = Vue.extend(repoEditor);
- return new RepoEditor().$mount();
- }
+ this.vm = new RepoEditor().$mount();
+ });
+
+ it('renders an ide container', (done) => {
+ this.vm.openedFiles = ['idiidid'];
+ this.vm.binary = false;
- it('renders an ide container', () => {
- const monacoInstance = jasmine.createSpyObj('monacoInstance', ['onMouseUp', 'onKeyUp', 'setModel', 'updateOptions']);
- const monaco = {
- editor: jasmine.createSpyObj('editor', ['create']),
- };
- RepoStore.monaco = monaco;
+ Vue.nextTick(() => {
+ expect(this.vm.shouldHideEditor).toBe(false);
+ expect(this.vm.$el.id).toEqual('ide');
+ expect(this.vm.$el.tagName).toBe('DIV');
+ done();
+ });
+ });
- monaco.editor.create.and.returnValue(monacoInstance);
- spyOn(repoEditor.watch, 'blobRaw');
+ describe('when there are no open files', () => {
+ it('does not render the ide', (done) => {
+ this.vm.openedFiles = [];
+
+ Vue.nextTick(() => {
+ expect(this.vm.shouldHideEditor).toBe(true);
+ expect(this.vm.$el.tagName).not.toBeDefined();
+ done();
+ });
+ });
+ });
- const vm = createComponent();
+ describe('when open file is binary and not raw', () => {
+ it('does not render the IDE', (done) => {
+ this.vm.binary = true;
+ this.vm.activeFile = {
+ raw: false,
+ };
- expect(vm.$el.id).toEqual('ide');
+ Vue.nextTick(() => {
+ expect(this.vm.shouldHideEditor).toBe(true);
+ expect(this.vm.$el.tagName).not.toBeDefined();
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js
index e1f25e4485f..dfab51710c3 100644
--- a/spec/javascripts/repo/components/repo_file_buttons_spec.js
+++ b/spec/javascripts/repo/components/repo_file_buttons_spec.js
@@ -23,6 +23,7 @@ describe('RepoFileButtons', () => {
RepoStore.activeFile = activeFile;
RepoStore.activeFileLabel = activeFileLabel;
RepoStore.editMode = true;
+ RepoStore.binary = false;
const vm = createComponent();
const raw = vm.$el.querySelector('.raw');
@@ -31,13 +32,13 @@ describe('RepoFileButtons', () => {
expect(vm.$el.id).toEqual('repo-file-buttons');
expect(raw.href).toMatch(`/${activeFile.raw_path}`);
- expect(raw.textContent).toEqual('Raw');
+ expect(raw.textContent.trim()).toEqual('Raw');
expect(blame.href).toMatch(`/${activeFile.blame_path}`);
- expect(blame.textContent).toEqual('Blame');
+ expect(blame.textContent.trim()).toEqual('Blame');
expect(history.href).toMatch(`/${activeFile.commits_path}`);
- expect(history.textContent).toEqual('History');
- expect(vm.$el.querySelector('.permalink').textContent).toEqual('Permalink');
- expect(vm.$el.querySelector('.preview').textContent).toEqual(activeFileLabel);
+ expect(history.textContent.trim()).toEqual('History');
+ expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink');
+ expect(vm.$el.querySelector('.preview').textContent.trim()).toEqual(activeFileLabel);
});
it('triggers rawPreviewToggle on preview click', () => {
@@ -71,12 +72,4 @@ describe('RepoFileButtons', () => {
expect(vm.$el.querySelector('.preview')).toBeFalsy();
});
-
- it('does not render if not isMini', () => {
- RepoStore.openedFiles = [];
-
- const vm = createComponent();
-
- expect(vm.$el.innerHTML).toBeFalsy();
- });
});
diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js
index 90616ae13ca..518a2d25ecf 100644
--- a/spec/javascripts/repo/components/repo_file_spec.js
+++ b/spec/javascripts/repo/components/repo_file_spec.js
@@ -39,9 +39,9 @@ describe('RepoFile', () => {
expect(vm.$el.querySelector(`.${file.icon}`).style.marginLeft).toEqual('100px');
expect(name.title).toEqual(file.url);
expect(name.href).toMatch(`/${file.url}`);
- expect(name.textContent).toEqual(file.name);
- expect(vm.$el.querySelector('.commit-message').textContent).toBe(file.lastCommitMessage);
- expect(vm.$el.querySelector('.commit-update').textContent).toBe(updated);
+ expect(name.textContent.trim()).toEqual(file.name);
+ expect(vm.$el.querySelector('.commit-message').textContent.trim()).toBe(file.lastCommitMessage);
+ expect(vm.$el.querySelector('.commit-update').textContent.trim()).toBe(updated);
expect(fileIcon.classList.contains(file.icon)).toBeTruthy();
expect(fileIcon.style.marginLeft).toEqual(`${file.level * 10}px`);
});
diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js
index d84f4c5609e..a030314d749 100644
--- a/spec/javascripts/repo/components/repo_loading_file_spec.js
+++ b/spec/javascripts/repo/components/repo_loading_file_spec.js
@@ -13,7 +13,7 @@ describe('RepoLoadingFile', () => {
function assertLines(lines) {
lines.forEach((line, n) => {
const index = n + 1;
- expect(line.classList.contains(`line-of-code-${index}`)).toBeTruthy();
+ expect(line.classList.contains(`skeleton-line-${index}`)).toBeTruthy();
});
}
diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js
index 0d216c9c026..abcff8e537e 100644
--- a/spec/javascripts/repo/components/repo_sidebar_spec.js
+++ b/spec/javascripts/repo/components/repo_sidebar_spec.js
@@ -1,4 +1,6 @@
import Vue from 'vue';
+import Helper from '~/repo/helpers/repo_helper';
+import RepoService from '~/repo/services/repo_service';
import RepoStore from '~/repo/stores/repo_store';
import repoSidebar from '~/repo/components/repo_sidebar.vue';
@@ -13,6 +15,7 @@ describe('RepoSidebar', () => {
RepoStore.files = [{
id: 0,
}];
+ RepoStore.openedFiles = [];
const vm = createComponent();
const thead = vm.$el.querySelector('thead');
const tbody = vm.$el.querySelector('tbody');
@@ -58,4 +61,51 @@ describe('RepoSidebar', () => {
expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
});
+
+ describe('methods', () => {
+ describe('fileClicked', () => {
+ it('should fetch data for new file', () => {
+ spyOn(Helper, 'getContent').and.callThrough();
+ const file1 = {
+ id: 0,
+ url: '',
+ };
+ RepoStore.files = [file1];
+ RepoStore.isRoot = true;
+ const vm = createComponent();
+
+ vm.fileClicked(file1);
+
+ expect(Helper.getContent).toHaveBeenCalledWith(file1);
+ });
+
+ it('should hide files in directory if already open', () => {
+ spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough();
+ const file1 = {
+ id: 0,
+ type: 'tree',
+ url: '',
+ opened: true,
+ };
+ RepoStore.files = [file1];
+ RepoStore.isRoot = true;
+ const vm = createComponent();
+
+ vm.fileClicked(file1);
+
+ expect(RepoStore.removeChildFilesOfTree).toHaveBeenCalledWith(file1);
+ });
+ });
+
+ describe('goToPreviousDirectoryClicked', () => {
+ it('should hide files in directory if already open', () => {
+ const prevUrl = 'foo/bar';
+ const vm = createComponent();
+
+ vm.goToPreviousDirectoryClicked(prevUrl);
+
+ expect(RepoService.url).toEqual(prevUrl);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js
index f3572804b4a..d2a790ad73a 100644
--- a/spec/javascripts/repo/components/repo_tab_spec.js
+++ b/spec/javascripts/repo/components/repo_tab_spec.js
@@ -12,7 +12,6 @@ describe('RepoTab', () => {
it('renders a close link and a name link', () => {
const tab = {
- loading: false,
url: 'url',
name: 'name',
};
@@ -22,38 +21,21 @@ describe('RepoTab', () => {
const close = vm.$el.querySelector('.close');
const name = vm.$el.querySelector(`a[title="${tab.url}"]`);
- spyOn(vm, 'xClicked');
+ spyOn(vm, 'closeTab');
spyOn(vm, 'tabClicked');
expect(close.querySelector('.fa-times')).toBeTruthy();
- expect(name.textContent).toEqual(tab.name);
+ expect(name.textContent.trim()).toEqual(tab.name);
close.click();
name.click();
- expect(vm.xClicked).toHaveBeenCalledWith(tab);
+ expect(vm.closeTab).toHaveBeenCalledWith(tab);
expect(vm.tabClicked).toHaveBeenCalledWith(tab);
});
- it('renders a spinner if tab is loading', () => {
- const tab = {
- loading: true,
- url: 'url',
- };
- const vm = createComponent({
- tab,
- });
- const close = vm.$el.querySelector('.close');
- const name = vm.$el.querySelector(`a[title="${tab.url}"]`);
-
- expect(close).toBeFalsy();
- expect(name).toBeFalsy();
- expect(vm.$el.querySelector('.fa.fa-spinner.fa-spin')).toBeTruthy();
- });
-
it('renders an fa-circle icon if tab is changed', () => {
const tab = {
- loading: false,
url: 'url',
name: 'name',
changed: true,
@@ -66,22 +48,22 @@ describe('RepoTab', () => {
});
describe('methods', () => {
- describe('xClicked', () => {
+ describe('closeTab', () => {
const vm = jasmine.createSpyObj('vm', ['$emit']);
it('returns undefined and does not $emit if file is changed', () => {
const file = { changed: true };
- const returnVal = repoTab.methods.xClicked.call(vm, file);
+ const returnVal = repoTab.methods.closeTab.call(vm, file);
expect(returnVal).toBeUndefined();
expect(vm.$emit).not.toHaveBeenCalled();
});
- it('$emits xclicked event with file obj', () => {
+ it('$emits tabclosed event with file obj', () => {
const file = { changed: false };
- repoTab.methods.xClicked.call(vm, file);
+ repoTab.methods.closeTab.call(vm, file);
- expect(vm.$emit).toHaveBeenCalledWith('xclicked', file);
+ expect(vm.$emit).toHaveBeenCalledWith('tabclosed', file);
});
});
});
diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js
index fdb12cfc00f..a02b54efafc 100644
--- a/spec/javascripts/repo/components/repo_tabs_spec.js
+++ b/spec/javascripts/repo/components/repo_tabs_spec.js
@@ -18,44 +18,25 @@ describe('RepoTabs', () => {
it('renders a list of tabs', () => {
RepoStore.openedFiles = openedFiles;
- RepoStore.tabsOverflow = true;
const vm = createComponent();
const tabs = [...vm.$el.querySelectorAll(':scope > li')];
expect(vm.$el.id).toEqual('tabs');
- expect(vm.$el.classList.contains('overflown')).toBeTruthy();
expect(tabs.length).toEqual(3);
expect(tabs[0].classList.contains('active')).toBeTruthy();
expect(tabs[1].classList.contains('active')).toBeFalsy();
expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy();
});
- it('does not render a tabs list if not isMini', () => {
- RepoStore.openedFiles = [];
-
- const vm = createComponent();
-
- expect(vm.$el.innerHTML).toBeFalsy();
- });
-
- it('does not apply overflown class if not tabsOverflow', () => {
- RepoStore.openedFiles = openedFiles;
- RepoStore.tabsOverflow = false;
-
- const vm = createComponent();
-
- expect(vm.$el.classList.contains('overflown')).toBeFalsy();
- });
-
describe('methods', () => {
- describe('xClicked', () => {
+ describe('tabClosed', () => {
it('calls removeFromOpenedFiles with file obj', () => {
const file = {};
spyOn(RepoStore, 'removeFromOpenedFiles');
- repoTabs.methods.xClicked(file);
+ repoTabs.methods.tabClosed(file);
expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(file);
});
diff --git a/spec/javascripts/repo/monaco_loader_spec.js b/spec/javascripts/repo/monaco_loader_spec.js
index be6e779c50f..887a80160fc 100644
--- a/spec/javascripts/repo/monaco_loader_spec.js
+++ b/spec/javascripts/repo/monaco_loader_spec.js
@@ -1,17 +1,13 @@
-/* global __webpack_public_path__ */
import monacoContext from 'monaco-editor/dev/vs/loader';
+import monacoLoader from '~/repo/monaco_loader';
describe('MonacoLoader', () => {
it('calls require.config and exports require', () => {
- spyOn(monacoContext.require, 'config');
-
- const monacoLoader = require('~/repo/monaco_loader'); // eslint-disable-line global-require
-
- expect(monacoContext.require.config).toHaveBeenCalledWith({
+ expect(monacoContext.require.getConfig()).toEqual(jasmine.objectContaining({
paths: {
vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
},
- });
- expect(monacoLoader.default).toBe(monacoContext.require);
+ }));
+ expect(monacoLoader).toBe(monacoContext.require);
});
});
diff --git a/spec/javascripts/repo/services/repo_service_spec.js b/spec/javascripts/repo/services/repo_service_spec.js
index d74e6a67b1e..6f530770525 100644
--- a/spec/javascripts/repo/services/repo_service_spec.js
+++ b/spec/javascripts/repo/services/repo_service_spec.js
@@ -1,5 +1,7 @@
import axios from 'axios';
import RepoService from '~/repo/services/repo_service';
+import RepoStore from '~/repo/stores/repo_store';
+import Api from '~/api';
describe('RepoService', () => {
it('has default json format param', () => {
@@ -118,4 +120,52 @@ describe('RepoService', () => {
}).catch(done.fail);
});
});
+
+ describe('commitFiles', () => {
+ it('calls commitMultiple and .then commitFlash', (done) => {
+ const projectId = 'projectId';
+ const payload = {};
+ RepoStore.projectId = projectId;
+
+ spyOn(Api, 'commitMultiple').and.returnValue(Promise.resolve());
+ spyOn(RepoService, 'commitFlash');
+
+ const apiPromise = RepoService.commitFiles(payload);
+
+ expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, payload);
+
+ apiPromise.then(() => {
+ expect(RepoService.commitFlash).toHaveBeenCalled();
+ done();
+ }).catch(done.fail);
+ });
+ });
+
+ describe('commitFlash', () => {
+ it('calls Flash with data.message', () => {
+ const data = {
+ message: 'message',
+ };
+ spyOn(window, 'Flash');
+
+ RepoService.commitFlash(data);
+
+ expect(window.Flash).toHaveBeenCalledWith(data.message);
+ });
+
+ it('calls Flash with success string if short_id and stats', () => {
+ const data = {
+ short_id: 'short_id',
+ stats: {
+ additions: '4',
+ deletions: '5',
+ },
+ };
+ spyOn(window, 'Flash');
+
+ RepoService.commitFlash(data);
+
+ expect(window.Flash).toHaveBeenCalledWith(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
+ });
+ });
});
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 3515dfbc60b..a912e150e9b 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,78 +1,74 @@
-/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */
/* global ShortcutsIssuable */
import '~/copy_as_gfm';
import '~/shortcuts_issuable';
-(function() {
- describe('ShortcutsIssuable', function() {
- var fixtureName = 'issues/open-issue.html.raw';
- preloadFixtures(fixtureName);
- beforeEach(function() {
- loadFixtures(fixtureName);
- document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
- this.shortcut = new ShortcutsIssuable();
- });
- describe('replyWithSelectedText', function() {
- var stubSelection;
- // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML.
- stubSelection = function(html) {
- window.gl.utils.getSelectedFragment = function() {
- var node = document.createElement('div');
- node.innerHTML = html;
- return node;
- };
+describe('ShortcutsIssuable', () => {
+ const fixtureName = 'merge_requests/diff_comment.html.raw';
+ preloadFixtures(fixtureName);
+ beforeEach(() => {
+ loadFixtures(fixtureName);
+ document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
+ this.shortcut = new ShortcutsIssuable(true);
+ });
+ describe('replyWithSelectedText', () => {
+ // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML.
+ const stubSelection = (html) => {
+ window.gl.utils.getSelectedFragment = () => {
+ const node = document.createElement('div');
+ node.innerHTML = html;
+ return node;
};
- beforeEach(function() {
- this.selector = 'form.js-main-target-form textarea#note_note';
+ };
+ beforeEach(() => {
+ this.selector = '.js-main-target-form #note_note';
+ });
+ describe('with empty selection', () => {
+ it('does not return an error', () => {
+ this.shortcut.replyWithSelectedText(true);
+ expect($(this.selector).val()).toBe('');
});
- describe('with empty selection', function() {
- it('does not return an error', function() {
- this.shortcut.replyWithSelectedText();
- expect($(this.selector).val()).toBe('');
- });
- it('triggers `focus`', function() {
- this.shortcut.replyWithSelectedText();
- expect(document.activeElement).toBe(document.querySelector(this.selector));
- });
+ it('triggers `focus`', () => {
+ this.shortcut.replyWithSelectedText(true);
+ expect(document.activeElement).toBe(document.querySelector(this.selector));
});
- describe('with any selection', function() {
- beforeEach(function() {
- stubSelection('<p>Selected text.</p>');
- });
- it('leaves existing input intact', function() {
- $(this.selector).val('This text was already here.');
- expect($(this.selector).val()).toBe('This text was already here.');
- this.shortcut.replyWithSelectedText();
- expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n");
- });
- it('triggers `input`', function() {
- var triggered = false;
- $(this.selector).on('input', function() {
- triggered = true;
- });
- this.shortcut.replyWithSelectedText();
- expect(triggered).toBe(true);
- });
- it('triggers `focus`', function() {
- this.shortcut.replyWithSelectedText();
- expect(document.activeElement).toBe(document.querySelector(this.selector));
- });
+ });
+ describe('with any selection', () => {
+ beforeEach(() => {
+ stubSelection('<p>Selected text.</p>');
});
- describe('with a one-line selection', function() {
- it('quotes the selection', function() {
- stubSelection('<p>This text has been selected.</p>');
- this.shortcut.replyWithSelectedText();
- expect($(this.selector).val()).toBe("> This text has been selected.\n\n");
- });
+ it('leaves existing input intact', () => {
+ $(this.selector).val('This text was already here.');
+ expect($(this.selector).val()).toBe('This text was already here.');
+ this.shortcut.replyWithSelectedText(true);
+ expect($(this.selector).val()).toBe('This text was already here.\n\n> Selected text.\n\n');
});
- describe('with a multi-line selection', function() {
- it('quotes the selected lines as a group', function() {
- stubSelection("<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>");
- this.shortcut.replyWithSelectedText();
- expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n");
+ it('triggers `input`', () => {
+ let triggered = false;
+ $(this.selector).on('input', () => {
+ triggered = true;
});
+ this.shortcut.replyWithSelectedText(true);
+ expect(triggered).toBe(true);
+ });
+ it('triggers `focus`', () => {
+ this.shortcut.replyWithSelectedText(true);
+ expect(document.activeElement).toBe(document.querySelector(this.selector));
+ });
+ });
+ describe('with a one-line selection', () => {
+ it('quotes the selection', () => {
+ stubSelection('<p>This text has been selected.</p>');
+ this.shortcut.replyWithSelectedText(true);
+ expect($(this.selector).val()).toBe('> This text has been selected.\n\n');
+ });
+ });
+ describe('with a multi-line selection', () => {
+ it('quotes the selected lines as a group', () => {
+ stubSelection('<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>');
+ this.shortcut.replyWithSelectedText(true);
+ expect($(this.selector).val()).toBe('> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n');
});
});
});
-}).call(window);
+});
diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js
index 9b8373df29e..53e4c68beb3 100644
--- a/spec/javascripts/shortcuts_spec.js
+++ b/spec/javascripts/shortcuts_spec.js
@@ -1,6 +1,6 @@
/* global Shortcuts */
describe('Shortcuts', () => {
- const fixtureName = 'issues/issue_with_comment.html.raw';
+ const fixtureName = 'merge_requests/diff_comment.html.raw';
const createEvent = (type, target) => $.Event(type, {
target,
});
diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
index 90eac1ed1ab..88a33caf2e3 100644
--- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
+++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
@@ -41,7 +41,7 @@ describe('Confidential Issue Sidebar Block', () => {
).toBe(true);
expect(
- vm2.$el.innerHTML.includes('None'),
+ vm2.$el.innerHTML.includes('Not confidential'),
).toBe(true);
});
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index 9fc8667ecc9..e2b6bcabc98 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -66,17 +66,57 @@ const sidebarMockData = {
},
labels: [],
},
+ '/autocomplete/projects?project_id=15': [
+ {
+ 'id': 0,
+ 'name_with_namespace': 'No project',
+ }, {
+ 'id': 20,
+ 'name_with_namespace': 'foo / bar',
+ },
+ ],
},
'PUT': {
'/gitlab-org/gitlab-shell/issues/5.json': {
data: {},
},
},
+ 'POST': {
+ '/gitlab-org/gitlab-shell/issues/5/move': {
+ id: 123,
+ iid: 5,
+ author_id: 1,
+ description: 'some description',
+ lock_version: 5,
+ milestone_id: null,
+ state: 'opened',
+ title: 'some title',
+ updated_by_id: 1,
+ created_at: '2017-06-27T19:54:42.437Z',
+ updated_at: '2017-08-18T03:39:49.222Z',
+ deleted_at: null,
+ time_estimate: 0,
+ total_time_spent: 0,
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ branch_name: null,
+ confidential: false,
+ assignees: [],
+ due_date: null,
+ moved_to_id: null,
+ project_id: 7,
+ milestone: null,
+ labels: [],
+ web_url: '/root/some-project/issues/5',
+ },
+ },
};
export default {
mediator: {
endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+ moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
+ projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
editable: true,
currentUser: {
id: 1,
@@ -85,6 +125,7 @@ export default {
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
rootPath: '/',
+ fullPath: '/gitlab-org/gitlab-shell',
},
time: {
time_estimate: 3600,
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index e246f41ee82..3aa8ca5db0d 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -30,7 +30,7 @@ describe('Sidebar mediator', () => {
expect(resp.status).toEqual(200);
done();
})
- .catch(() => {});
+ .catch(done.fail);
});
it('fetches the data', () => {
@@ -38,4 +38,42 @@ describe('Sidebar mediator', () => {
this.mediator.fetch();
expect(this.mediator.service.get).toHaveBeenCalled();
});
+
+ it('sets moveToProjectId', () => {
+ const projectId = 7;
+ spyOn(this.mediator.store, 'setMoveToProjectId').and.callThrough();
+
+ this.mediator.setMoveToProjectId(projectId);
+
+ expect(this.mediator.store.setMoveToProjectId).toHaveBeenCalledWith(projectId);
+ });
+
+ it('fetches autocomplete projects', (done) => {
+ const searchTerm = 'foo';
+ spyOn(this.mediator.service, 'getProjectsAutocomplete').and.callThrough();
+ spyOn(this.mediator.store, 'setAutocompleteProjects').and.callThrough();
+
+ this.mediator.fetchAutocompleteProjects(searchTerm)
+ .then(() => {
+ expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm);
+ expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled();
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('moves issue', (done) => {
+ const moveToProjectId = 7;
+ this.mediator.store.setMoveToProjectId(moveToProjectId);
+ spyOn(this.mediator.service, 'moveIssue').and.callThrough();
+ spyOn(gl.utils, 'visitUrl');
+
+ this.mediator.moveIssue()
+ .then(() => {
+ expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
+ expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
+ done();
+ })
+ .catch(done.fail);
+ });
});
diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
new file mode 100644
index 00000000000..8b0d51bbcc8
--- /dev/null
+++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
@@ -0,0 +1,142 @@
+import Vue from 'vue';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
+import Mock from './mock_data';
+
+describe('SidebarMoveIssue', () => {
+ beforeEach(() => {
+ Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
+ this.mediator = new SidebarMediator(Mock.mediator);
+ this.$content = $(`
+ <div class="dropdown">
+ <div class="js-toggle"></div>
+ <div class="dropdown-content"></div>
+ <div class="js-confirm-button"></div>
+ </div>
+ `);
+ this.$toggleButton = this.$content.find('.js-toggle');
+ this.$confirmButton = this.$content.find('.js-confirm-button');
+
+ this.sidebarMoveIssue = new SidebarMoveIssue(
+ this.mediator,
+ this.$toggleButton,
+ this.$confirmButton,
+ );
+ this.sidebarMoveIssue.init();
+ });
+
+ afterEach(() => {
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+
+ this.sidebarMoveIssue.destroy();
+
+ Vue.http.interceptors = _.without(Vue.http.interceptors, Mock.sidebarMockInterceptor);
+ });
+
+ describe('init', () => {
+ it('should initialize the dropdown and listeners', () => {
+ spyOn(this.sidebarMoveIssue, 'initDropdown');
+ spyOn(this.sidebarMoveIssue, 'addEventListeners');
+
+ this.sidebarMoveIssue.init();
+
+ expect(this.sidebarMoveIssue.initDropdown).toHaveBeenCalled();
+ expect(this.sidebarMoveIssue.addEventListeners).toHaveBeenCalled();
+ });
+ });
+
+ describe('destroy', () => {
+ it('should remove the listeners', () => {
+ spyOn(this.sidebarMoveIssue, 'removeEventListeners');
+
+ this.sidebarMoveIssue.destroy();
+
+ expect(this.sidebarMoveIssue.removeEventListeners).toHaveBeenCalled();
+ });
+ });
+
+ describe('initDropdown', () => {
+ it('should initialize the gl_dropdown', () => {
+ spyOn($.fn, 'glDropdown');
+
+ this.sidebarMoveIssue.initDropdown();
+
+ expect($.fn.glDropdown).toHaveBeenCalled();
+ });
+ });
+
+ describe('onConfirmClicked', () => {
+ it('should move the issue with valid project ID', () => {
+ spyOn(this.mediator, 'moveIssue').and.returnValue(Promise.resolve());
+ this.mediator.setMoveToProjectId(7);
+
+ this.sidebarMoveIssue.onConfirmClicked();
+
+ expect(this.mediator.moveIssue).toHaveBeenCalled();
+ expect(this.$confirmButton.attr('disabled')).toBe('disabled');
+ expect(this.$confirmButton.hasClass('is-loading')).toBe(true);
+ });
+
+ it('should remove loading state from confirm button on failure', (done) => {
+ spyOn(window, 'Flash');
+ spyOn(this.mediator, 'moveIssue').and.returnValue(Promise.reject());
+ this.mediator.setMoveToProjectId(7);
+
+ this.sidebarMoveIssue.onConfirmClicked();
+
+ expect(this.mediator.moveIssue).toHaveBeenCalled();
+ // Wait for the move issue request to fail
+ setTimeout(() => {
+ expect(window.Flash).toHaveBeenCalled();
+ expect(this.$confirmButton.attr('disabled')).toBe(undefined);
+ expect(this.$confirmButton.hasClass('is-loading')).toBe(false);
+ done();
+ });
+ });
+
+ it('should not move the issue with id=0', () => {
+ spyOn(this.mediator, 'moveIssue');
+ this.mediator.setMoveToProjectId(0);
+
+ this.sidebarMoveIssue.onConfirmClicked();
+
+ expect(this.mediator.moveIssue).not.toHaveBeenCalled();
+ });
+ });
+
+ it('should set moveToProjectId on dropdown item "No project" click', (done) => {
+ spyOn(this.mediator, 'setMoveToProjectId');
+
+ // Open the dropdown
+ this.$toggleButton.dropdown('toggle');
+
+ // Wait for the autocomplete request to finish
+ setTimeout(() => {
+ this.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click');
+
+ expect(this.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
+ expect(this.$confirmButton.attr('disabled')).toBe('disabled');
+ done();
+ }, 0);
+ });
+
+ it('should set moveToProjectId on dropdown item click', (done) => {
+ spyOn(this.mediator, 'setMoveToProjectId');
+
+ // Open the dropdown
+ this.$toggleButton.dropdown('toggle');
+
+ // Wait for the autocomplete request to finish
+ setTimeout(() => {
+ this.$content.find('.js-move-issue-dropdown-item').eq(1).trigger('click');
+
+ expect(this.mediator.setMoveToProjectId).toHaveBeenCalledWith(20);
+ expect(this.$confirmButton.attr('disabled')).toBe(undefined);
+ done();
+ }, 0);
+ });
+});
diff --git a/spec/javascripts/sidebar/sidebar_service_spec.js b/spec/javascripts/sidebar/sidebar_service_spec.js
index 91a4dd669a7..a4bd8ba8d88 100644
--- a/spec/javascripts/sidebar/sidebar_service_spec.js
+++ b/spec/javascripts/sidebar/sidebar_service_spec.js
@@ -5,7 +5,11 @@ import Mock from './mock_data';
describe('Sidebar service', () => {
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
- this.service = new SidebarService('/gitlab-org/gitlab-shell/issues/5.json');
+ this.service = new SidebarService({
+ endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+ moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
+ projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
+ });
});
afterEach(() => {
@@ -19,7 +23,7 @@ describe('Sidebar service', () => {
expect(resp).toBeDefined();
done();
})
- .catch(() => {});
+ .catch(done.fail);
});
it('updates the data', (done) => {
@@ -28,6 +32,24 @@ describe('Sidebar service', () => {
expect(resp).toBeDefined();
done();
})
- .catch(() => {});
+ .catch(done.fail);
+ });
+
+ it('gets projects for autocomplete', (done) => {
+ this.service.getProjectsAutocomplete()
+ .then((resp) => {
+ expect(resp).toBeDefined();
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('moves the issue to another project', (done) => {
+ this.service.moveIssue(123)
+ .then((resp) => {
+ expect(resp).toBeDefined();
+ done();
+ })
+ .catch(done.fail);
});
});
diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js
index b3fa156eb64..69eb3839d67 100644
--- a/spec/javascripts/sidebar/sidebar_store_spec.js
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -82,4 +82,18 @@ describe('Sidebar store', () => {
expect(this.store.humanTimeEstimate).toEqual(Mock.time.human_time_estimate);
expect(this.store.humanTotalTimeSpent).toEqual(Mock.time.human_total_time_spent);
});
+
+ it('set autocomplete projects', () => {
+ const projects = [{ id: 0 }];
+ this.store.setAutocompleteProjects(projects);
+
+ expect(this.store.autocompleteProjects).toEqual(projects);
+ });
+
+ it('set move to project ID', () => {
+ const projectId = 7;
+ this.store.setMoveToProjectId(projectId);
+
+ expect(this.store.moveToProjectId).toEqual(projectId);
+ });
});
diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js
index 28d0c7dcd99..69cb93bd850 100644
--- a/spec/javascripts/user_callout_spec.js
+++ b/spec/javascripts/user_callout_spec.js
@@ -33,4 +33,17 @@ describe('UserCallout', function () {
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_shared/components/identicon_spec.js b/spec/javascripts/vue_shared/components/identicon_spec.js
new file mode 100644
index 00000000000..647680f00f7
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/identicon_spec.js
@@ -0,0 +1,72 @@
+import Vue from 'vue';
+import identiconComponent from '~/vue_shared/components/identicon.vue';
+
+const createComponent = (sizeClass) => {
+ const Component = Vue.extend(identiconComponent);
+
+ return new Component({
+ propsData: {
+ entityId: 1,
+ entityName: 'entity-name',
+ sizeClass,
+ },
+ }).$mount();
+};
+
+describe('IdenticonComponent', () => {
+ describe('computed', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('identiconStyles', () => {
+ it('should return styles attribute value with `background-color` property', () => {
+ vm.entityId = 4;
+
+ expect(vm.identiconStyles).toBeDefined();
+ expect(vm.identiconStyles.indexOf('background-color: #E0F2F1;') > -1).toBeTruthy();
+ });
+
+ it('should return styles attribute value with `color` property', () => {
+ vm.entityId = 4;
+
+ expect(vm.identiconStyles).toBeDefined();
+ expect(vm.identiconStyles.indexOf('color: #555;') > -1).toBeTruthy();
+ });
+ });
+
+ describe('identiconTitle', () => {
+ it('should return first letter of entity title in uppercase', () => {
+ vm.entityName = 'dummy-group';
+
+ expect(vm.identiconTitle).toBeDefined();
+ expect(vm.identiconTitle).toBe('D');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render identicon', () => {
+ const vm = createComponent();
+
+ expect(vm.$el.nodeName).toBe('DIV');
+ expect(vm.$el.classList.contains('identicon')).toBeTruthy();
+ expect(vm.$el.classList.contains('s40')).toBeTruthy();
+ expect(vm.$el.getAttribute('style').indexOf('background-color') > -1).toBeTruthy();
+ vm.$destroy();
+ });
+
+ it('should render identicon with provided sizing class', () => {
+ const vm = createComponent('s32');
+
+ expect(vm.$el.classList.contains('s32')).toBeTruthy();
+ vm.$destroy();
+ });
+ });
+});
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
new file mode 100644
index 00000000000..6df08f3ebe7
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js
@@ -0,0 +1,20 @@
+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/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js
index 291e19c9f3c..60a5c2ae74e 100644
--- a/spec/javascripts/vue_shared/components/markdown/field_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js
@@ -16,8 +16,8 @@ describe('Markdown field component', () => {
},
template: `
<field-component
- marodown-preview-url="/preview"
- markdown-docs="/docs"
+ markdown-preview-path="/preview"
+ markdown-docs-path="/docs"
>
<textarea
slot="textarea"
@@ -92,6 +92,7 @@ describe('Markdown field component', () => {
it('renders GFM with jQuery', (done) => {
spyOn($.fn, 'renderGFM');
+
previewLink.click();
setTimeout(() => {
@@ -100,7 +101,7 @@ describe('Markdown field component', () => {
).toHaveBeenCalled();
done();
- });
+ }, 0);
});
});
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index a225b04c47e..bd18f79cea7 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -8,7 +8,7 @@ import ZenMode from '~/zen_mode';
var enterZen, escapeKeydown, exitZen;
describe('ZenMode', function() {
- var fixtureName = 'issues/open-issue.html.raw';
+ var fixtureName = 'merge_requests/merge_request_with_comment.html.raw';
preloadFixtures(fixtureName);
beforeEach(function() {
loadFixtures(fixtureName);
diff --git a/spec/lib/after_commit_queue_spec.rb b/spec/lib/after_commit_queue_spec.rb
new file mode 100644
index 00000000000..6e7c2ec2363
--- /dev/null
+++ b/spec/lib/after_commit_queue_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe AfterCommitQueue do
+ it 'runs after transaction is committed' do
+ called = false
+ test_proc = proc { called = true }
+
+ project = build(:project)
+ project.run_after_commit(&test_proc)
+
+ project.save
+
+ expect(called).to be true
+ end
+end
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb
index fb3ef04b860..59deca7757b 100644
--- a/spec/lib/api/helpers/pagination_spec.rb
+++ b/spec/lib/api/helpers/pagination_spec.rb
@@ -52,7 +52,13 @@ describe API::Helpers::Pagination do
expect_header('X-Page', '1')
expect_header('X-Next-Page', '2')
expect_header('X-Prev-Page', '')
- expect_header('Link', any_args)
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include('rel="first"')
+ expect(val).to include('rel="last"')
+ expect(val).to include('rel="next"')
+ expect(val).not_to include('rel="prev"')
+ end
subject.paginate(resource)
end
@@ -75,15 +81,53 @@ describe API::Helpers::Pagination do
expect_header('X-Page', '2')
expect_header('X-Next-Page', '')
expect_header('X-Prev-Page', '1')
- expect_header('Link', any_args)
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include('rel="first"')
+ expect(val).to include('rel="last"')
+ expect(val).to include('rel="prev"')
+ expect(val).not_to include('rel="next"')
+ end
+
+ subject.paginate(resource)
+ end
+ end
+ end
+
+ context 'when resource empty' do
+ describe 'first page' do
+ before do
+ allow(subject).to receive(:params)
+ .and_return({ page: 1, per_page: 2 })
+ end
+
+ it 'returns appropriate amount of resources' do
+ expect(subject.paginate(resource).count).to eq 0
+ end
+
+ it 'adds appropriate headers' do
+ expect_header('X-Total', '0')
+ expect_header('X-Total-Pages', '1')
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Page', '1')
+ expect_header('X-Next-Page', '')
+ expect_header('X-Prev-Page', '')
+
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include('rel="first"')
+ expect(val).to include('rel="last"')
+ expect(val).not_to include('rel="prev"')
+ expect(val).not_to include('rel="next"')
+ expect(val).not_to include('page=0')
+ end
subject.paginate(resource)
end
end
end
- def expect_header(name, value)
- expect(subject).to receive(:header).with(name, value)
+ def expect_header(*args, &block)
+ expect(subject).to receive(:header).with(*args, &block)
end
def expect_message(method)
diff --git a/spec/lib/banzai/commit_renderer_spec.rb b/spec/lib/banzai/commit_renderer_spec.rb
new file mode 100644
index 00000000000..049d025a5b9
--- /dev/null
+++ b/spec/lib/banzai/commit_renderer_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Banzai::CommitRenderer do
+ describe '.render' do
+ it 'renders a commit description and title' do
+ user = double(:user)
+ project = create(:project, :repository)
+
+ expect(Banzai::ObjectRenderer).to receive(:new).with(project, user).and_call_original
+
+ described_class::ATTRIBUTES.each do |attr|
+ expect_any_instance_of(Banzai::ObjectRenderer).to receive(:render).with([project.commit], attr).once.and_call_original
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr)
+ end
+
+ described_class.render([project.commit], project, user)
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index ff6b19459bb..85eddde732e 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -96,5 +96,41 @@ describe Banzai::Filter::TableOfContentsFilter do
expect(links.last.attr('href')).to eq '#header-2'
expect(links.last.text).to eq 'Header 2'
end
+
+ context 'table of contents nesting' do
+ let(:results) do
+ result(
+ header(1, 'Header 1') <<
+ header(2, 'Header 1-1') <<
+ header(3, 'Header 1-1-1') <<
+ header(2, 'Header 1-2') <<
+ header(1, 'Header 2') <<
+ header(2, 'Header 2-1')
+ )
+ end
+
+ it 'keeps list levels regarding header levels' do
+ items = doc.css('li')
+
+ # Header 1
+ expect(items[0].ancestors).to satisfy_none { |node| node.name == 'li' }
+
+ # Header 1-1
+ expect(items[1].ancestors).to include(items[0])
+
+ # Header 1-1-1
+ expect(items[2].ancestors).to include(items[0], items[1])
+
+ # Header 1-2
+ expect(items[3].ancestors).to include(items[0])
+ expect(items[3].ancestors).not_to include(items[1])
+
+ # Header 2
+ expect(items[4].ancestors).to satisfy_none { |node| node.name == 'li' }
+
+ # Header 2-1
+ expect(items[5].ancestors).to include(items[4])
+ end
+ end
end
end
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index 7f5d481c36c..b172a1b718c 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -1,53 +1,77 @@
require 'spec_helper'
describe Banzai::ObjectRenderer do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { project.owner }
let(:renderer) { described_class.new(project, user, custom_value: 'value') }
let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_VERSION) }
describe '#render' do
- it 'renders and redacts an Array of objects' do
- renderer.render([object], :note)
+ context 'with cache' do
+ it 'renders and redacts an Array of objects' do
+ renderer.render([object], :note)
- expect(object.redacted_note_html).to eq '<p dir="auto">hello</p>'
- expect(object.user_visible_reference_count).to eq 0
- end
+ expect(object.redacted_note_html).to eq '<p dir="auto">hello</p>'
+ expect(object.user_visible_reference_count).to eq 0
+ end
- it 'calls Banzai::Redactor to perform redaction' do
- expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original
+ it 'calls Banzai::Redactor to perform redaction' do
+ expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original
- renderer.render([object], :note)
- end
+ renderer.render([object], :note)
+ end
- it 'retrieves field content using Banzai.render_field' do
- expect(Banzai).to receive(:render_field).with(object, :note).and_call_original
+ it 'retrieves field content using Banzai::Renderer.render_field' do
+ expect(Banzai::Renderer).to receive(:render_field).with(object, :note).and_call_original
- renderer.render([object], :note)
- end
+ renderer.render([object], :note)
+ end
- it 'passes context to PostProcessPipeline' do
- another_user = create(:user)
- another_project = create(:project)
- object = Note.new(
- note: 'hello',
- note_html: 'hello',
- author: another_user,
- project: another_project
- )
-
- expect(Banzai::Pipeline::PostProcessPipeline).to receive(:to_document).with(
- anything,
- hash_including(
- skip_redaction: true,
- current_user: user,
- project: another_project,
+ it 'passes context to PostProcessPipeline' do
+ another_user = create(:user)
+ another_project = create(:project)
+ object = Note.new(
+ note: 'hello',
+ note_html: 'hello',
author: another_user,
- custom_value: 'value'
+ project: another_project
)
- ).and_call_original
- renderer.render([object], :note)
+ expect(Banzai::Pipeline::PostProcessPipeline).to receive(:to_document).with(
+ anything,
+ hash_including(
+ skip_redaction: true,
+ current_user: user,
+ project: another_project,
+ author: another_user,
+ custom_value: 'value'
+ )
+ ).and_call_original
+
+ renderer.render([object], :note)
+ end
+ end
+
+ context 'without cache' do
+ let(:commit) { project.commit }
+
+ it 'renders and redacts an Array of objects' do
+ renderer.render([commit], :title)
+
+ expect(commit.redacted_title_html).to eq("Merge branch 'branch-merged' into 'master'")
+ end
+
+ it 'calls Banzai::Redactor to perform redaction' do
+ expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original
+
+ renderer.render([commit], :title)
+ end
+
+ it 'retrieves field content using Banzai::Renderer.cacheless_render_field' do
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).with(commit, :title).and_call_original
+
+ renderer.render([commit], :title)
+ end
end
end
end
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index 0e094405e33..da42272bbef 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -4,6 +4,7 @@ describe Banzai::Renderer do
def fake_object(fresh:)
object = double('object')
+ allow(object).to receive(:respond_to?).with(:cached_markdown_fields).and_return(true)
allow(object).to receive(:cached_html_up_to_date?).with(:field).and_return(fresh)
allow(object).to receive(:cached_html_for).with(:field).and_return('field_html')
@@ -12,25 +13,38 @@ describe Banzai::Renderer do
describe '#render_field' do
let(:renderer) { described_class }
- subject { renderer.render_field(object, :field) }
- context 'with a stale cache' do
- let(:object) { fake_object(fresh: false) }
+ context 'without cache' do
+ let(:commit) { create(:project, :repository).commit }
- it 'caches and returns the result' do
- expect(object).to receive(:refresh_markdown_cache!).with(do_update: true)
+ it 'returns cacheless render field' do
+ expect(renderer).to receive(:cacheless_render_field).with(commit, :title)
- is_expected.to eq('field_html')
+ renderer.render_field(commit, :title)
end
end
- context 'with an up-to-date cache' do
- let(:object) { fake_object(fresh: true) }
+ context 'with cache' do
+ subject { renderer.render_field(object, :field) }
- it 'uses the cache' do
- expect(object).to receive(:refresh_markdown_cache!).never
+ context 'with a stale cache' do
+ let(:object) { fake_object(fresh: false) }
- is_expected.to eq('field_html')
+ it 'caches and returns the result' do
+ expect(object).to receive(:refresh_markdown_cache!).with(do_update: true)
+
+ is_expected.to eq('field_html')
+ end
+ end
+
+ context 'with an up-to-date cache' do
+ let(:object) { fake_object(fresh: true) }
+
+ it 'uses the cache' do
+ expect(object).to receive(:refresh_markdown_cache!).never
+
+ is_expected.to eq('field_html')
+ end
end
end
end
diff --git a/spec/lib/bitbucket/paginator_spec.rb b/spec/lib/bitbucket/paginator_spec.rb
index 2c972da682e..bdf10a5e2a2 100644
--- a/spec/lib/bitbucket/paginator_spec.rb
+++ b/spec/lib/bitbucket/paginator_spec.rb
@@ -15,7 +15,7 @@ describe Bitbucket::Paginator do
expect(paginator.items).to match(['item_2'])
allow(paginator).to receive(:fetch_next_page).and_return(nil)
- expect{ paginator.items }.to raise_error(StopIteration)
+ expect { paginator.items }.to raise_error(StopIteration)
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
index ed571a2ba05..1efd3113a43 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -164,9 +164,46 @@ module Ci
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_ref" do
+ describe "#builds_for_stage_and_ref" do
let(:type) { 'test' }
it "returns builds if no branch specified" do
@@ -344,28 +381,31 @@ module Ci
let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
- shared_examples 'raises an error' do
- 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 integer' do
let(:only) { 1 }
- it_behaves_like 'raises an error'
+ 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_behaves_like 'raises an error'
+ 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_behaves_like 'raises an error'
+ 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
@@ -518,28 +558,31 @@ module Ci
let(:config) { { rspec: { script: "rspec", except: except } } }
let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
- shared_examples 'raises an error' do
- 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 integer' do
let(:except) { 1 }
- it_behaves_like 'raises an error'
+ 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_behaves_like 'raises an error'
+ 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_behaves_like 'raises an error'
+ 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
@@ -1323,11 +1366,11 @@ EOT
describe "Error handling" do
it "fails to parse YAML" do
- expect{GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
+ 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)
+ expect {GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError)
end
it "returns errors if tags parameter is invalid" do
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index e76463b5e7c..cb4ae3be525 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe ContainerRegistry::Tag do
let(:group) { create(:group, name: 'group') }
- let(:project) { create(:project, :repository, path: 'test', group: group) }
+ let(:project) { create(:project, path: 'test', group: group) }
let(:repository) do
create(:container_repository, name: '', project: project)
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
index b0efcab47fb..87ae6b6cf01 100644
--- a/spec/lib/event_filter_spec.rb
+++ b/spec/lib/event_filter_spec.rb
@@ -5,7 +5,7 @@ describe EventFilter do
let(:source_user) { create(:user) }
let!(:public_project) { create(:project, :public) }
- let!(:push_event) { create(:event, :pushed, project: public_project, target: public_project, author: source_user) }
+ let!(:push_event) { create(:push_event, project: public_project, author: source_user) }
let!(:merged_event) { create(:event, :merged, project: public_project, target: public_project, author: source_user) }
let!(:created_event) { create(:event, :created, project: public_project, target: public_project, author: source_user) }
let!(:updated_event) { create(:event, :updated, project: public_project, target: public_project, author: source_user) }
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 867f7d55af7..e13406d1972 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -56,7 +56,7 @@ describe ExtractsPath do
context 'subclass overrides get_id' do
it 'uses ref returned by get_id' do
- allow_any_instance_of(self.class).to receive(:get_id){ '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' }
+ allow_any_instance_of(self.class).to receive(:get_id) { '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' }
assign_ref_vars
diff --git a/spec/lib/file_size_validator_spec.rb b/spec/lib/file_size_validator_spec.rb
index 49501931dd2..c44bc1840df 100644
--- a/spec/lib/file_size_validator_spec.rb
+++ b/spec/lib/file_size_validator_spec.rb
@@ -24,13 +24,13 @@ describe FileSizeValidator do
describe 'options uses a symbol' do
let(:options) do
{
- maximum: :test,
+ maximum: :max_attachment_size,
attributes: { attachment: attachment }
}
end
before do
- allow(note).to receive(:test) { 10 }
+ expect(note).to receive(:max_attachment_size) { 10 }
end
it 'attachment exceeds maximum limit' do
diff --git a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
index f29431b937c..22708687a56 100644
--- a/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
+++ b/spec/lib/gitlab/auth/unique_ips_limiter_spec.rb
@@ -41,7 +41,7 @@ describe Gitlab::Auth::UniqueIpsLimiter, :clean_gitlab_redis_shared_state do
context 'allow 2 unique ips' do
before do
- current_application_settings.update!(unique_ips_limit_per_user: 2)
+ Gitlab::CurrentSettings.current_application_settings.update!(unique_ips_limit_per_user: 2)
end
it 'blocks user trying to login from third ip' do
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 4a498e79c87..f685bb83d0d 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -279,16 +279,6 @@ describe Gitlab::Auth do
gl_auth.find_with_user_password('ldap_user', 'password')
end
end
-
- context "with sign-in disabled" do
- before do
- stub_application_setting(password_authentication_enabled: false)
- end
-
- it "does not find user by valid login/password" do
- expect(gl_auth.find_with_user_password(username, password)).to be_nil
- end
- end
end
private
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 18843cbe992..c0427639746 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
@@ -123,6 +123,28 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do
include_examples 'updated MR diff'
end
+ context 'when the merge request diffs do not have too_large set' do
+ let(:commits) { merge_request_diff.commits.map(&:to_hash) }
+ let(:expected_diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) }
+
+ let(:diffs) do
+ expected_diffs.map { |diff| diff.except(:too_large) }
+ end
+
+ include_examples 'updated MR diff'
+ end
+
+ context 'when the merge request diffs do not have a_mode and b_mode set' do
+ let(:commits) { merge_request_diff.commits.map(&:to_hash) }
+ let(:expected_diffs) { diffs_to_hashes(merge_request_diff.merge_request_diff_files) }
+
+ let(:diffs) do
+ expected_diffs.map { |diff| diff.except(:a_mode, :b_mode) }
+ end
+
+ include_examples 'updated MR diff'
+ end
+
context 'when the merge request diffs have binary content' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
let(:expected_diffs) { diffs }
@@ -170,7 +192,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do
context 'when the merge request diffs are Rugged::Patch instances' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) }
- let(:diffs) { first_commit.diff_from_parent.patches }
+ let(:diffs) { first_commit.rugged_diff_from_parent.patches }
let(:expected_diffs) { [] }
include_examples 'updated MR diff'
@@ -179,7 +201,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do
context 'when the merge request diffs are Rugged::Diff::Delta instances' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) }
- let(:diffs) { first_commit.diff_from_parent.deltas }
+ let(:diffs) { first_commit.rugged_diff_from_parent.deltas }
let(:expected_diffs) { [] }
include_examples 'updated MR diff'
diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
new file mode 100644
index 00000000000..cb52d971047
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
@@ -0,0 +1,420 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event, :migration, schema: 20170608152748 do
+ describe '#commit_title' do
+ it 'returns nil when there are no commits' do
+ expect(described_class.new.commit_title).to be_nil
+ end
+
+ it 'returns nil when there are commits without commit messages' do
+ event = described_class.new
+
+ allow(event).to receive(:commits).and_return([{ id: '123' }])
+
+ expect(event.commit_title).to be_nil
+ end
+
+ it 'returns the commit message when it is less than 70 characters long' do
+ event = described_class.new
+
+ allow(event).to receive(:commits).and_return([{ message: 'Hello world' }])
+
+ expect(event.commit_title).to eq('Hello world')
+ end
+
+ it 'returns the first line of a commit message if multiple lines are present' do
+ event = described_class.new
+
+ allow(event).to receive(:commits).and_return([{ message: "Hello\n\nworld" }])
+
+ expect(event.commit_title).to eq('Hello')
+ end
+
+ it 'truncates the commit to 70 characters when it is too long' do
+ event = described_class.new
+
+ allow(event).to receive(:commits).and_return([{ message: 'a' * 100 }])
+
+ expect(event.commit_title).to eq(('a' * 67) + '...')
+ end
+ end
+
+ describe '#commit_from_sha' do
+ it 'returns nil when pushing to a new ref' do
+ event = described_class.new
+
+ allow(event).to receive(:create?).and_return(true)
+
+ expect(event.commit_from_sha).to be_nil
+ end
+
+ it 'returns the ID of the first commit when pushing to an existing ref' do
+ event = described_class.new
+
+ allow(event).to receive(:create?).and_return(false)
+ allow(event).to receive(:data).and_return(before: '123')
+
+ expect(event.commit_from_sha).to eq('123')
+ end
+ end
+
+ describe '#commit_to_sha' do
+ it 'returns nil when removing an existing ref' do
+ event = described_class.new
+
+ allow(event).to receive(:remove?).and_return(true)
+
+ expect(event.commit_to_sha).to be_nil
+ end
+
+ it 'returns the ID of the last commit when pushing to an existing ref' do
+ event = described_class.new
+
+ allow(event).to receive(:remove?).and_return(false)
+ allow(event).to receive(:data).and_return(after: '123')
+
+ expect(event.commit_to_sha).to eq('123')
+ end
+ end
+
+ describe '#data' do
+ it 'returns the deserialized data' do
+ event = described_class.new(data: { before: '123' })
+
+ expect(event.data).to eq(before: '123')
+ end
+
+ it 'returns an empty hash when no data is present' do
+ event = described_class.new
+
+ expect(event.data).to eq({})
+ end
+ end
+
+ describe '#commits' do
+ it 'returns an Array of commits' do
+ event = described_class.new(data: { commits: [{ id: '123' }] })
+
+ expect(event.commits).to eq([{ id: '123' }])
+ end
+
+ it 'returns an empty array when no data is present' do
+ event = described_class.new
+
+ expect(event.commits).to eq([])
+ end
+ end
+
+ describe '#commit_count' do
+ it 'returns the number of commits' do
+ event = described_class.new(data: { total_commits_count: 2 })
+
+ expect(event.commit_count).to eq(2)
+ end
+
+ it 'returns 0 when no data is present' do
+ event = described_class.new
+
+ expect(event.commit_count).to eq(0)
+ end
+ end
+
+ describe '#ref' do
+ it 'returns the name of the ref' do
+ event = described_class.new(data: { ref: 'refs/heads/master' })
+
+ expect(event.ref).to eq('refs/heads/master')
+ end
+ end
+
+ describe '#trimmed_ref_name' do
+ it 'returns the trimmed ref name for a branch' do
+ event = described_class.new(data: { ref: 'refs/heads/master' })
+
+ expect(event.trimmed_ref_name).to eq('master')
+ end
+
+ it 'returns the trimmed ref name for a tag' do
+ event = described_class.new(data: { ref: 'refs/tags/v1.2' })
+
+ expect(event.trimmed_ref_name).to eq('v1.2')
+ end
+ end
+
+ describe '#create?' do
+ it 'returns true when creating a new ref' do
+ event = described_class.new(data: { before: described_class::BLANK_REF })
+
+ expect(event.create?).to eq(true)
+ end
+
+ it 'returns false when pushing to an existing ref' do
+ event = described_class.new(data: { before: '123' })
+
+ expect(event.create?).to eq(false)
+ end
+ end
+
+ describe '#remove?' do
+ it 'returns true when removing an existing ref' do
+ event = described_class.new(data: { after: described_class::BLANK_REF })
+
+ expect(event.remove?).to eq(true)
+ end
+
+ it 'returns false when pushing to an existing ref' do
+ event = described_class.new(data: { after: '123' })
+
+ expect(event.remove?).to eq(false)
+ end
+ end
+
+ describe '#push_action' do
+ let(:event) { described_class.new }
+
+ it 'returns :created when creating a new ref' do
+ allow(event).to receive(:create?).and_return(true)
+
+ expect(event.push_action).to eq(:created)
+ end
+
+ it 'returns :removed when removing an existing ref' do
+ allow(event).to receive(:create?).and_return(false)
+ allow(event).to receive(:remove?).and_return(true)
+
+ expect(event.push_action).to eq(:removed)
+ end
+
+ it 'returns :pushed when pushing to an existing ref' do
+ allow(event).to receive(:create?).and_return(false)
+ allow(event).to receive(:remove?).and_return(false)
+
+ expect(event.push_action).to eq(:pushed)
+ end
+ end
+
+ describe '#ref_type' do
+ let(:event) { described_class.new }
+
+ it 'returns :tag for a tag' do
+ allow(event).to receive(:ref).and_return('refs/tags/1.2')
+
+ expect(event.ref_type).to eq(:tag)
+ end
+
+ it 'returns :branch for a branch' do
+ allow(event).to receive(:ref).and_return('refs/heads/1.2')
+
+ expect(event.ref_type).to eq(:branch)
+ end
+ end
+end
+
+##
+# The background migration relies on a temporary table, hence we're migrating
+# to a specific version of the database where said table is still present.
+#
+describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170825154015 do
+ let(:user_class) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'users'
+ end
+ end
+
+ let(:migration) { described_class.new }
+ let(:user_class) { table(:users) }
+ let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) }
+ let(:namespace) { create(:namespace, owner: author) }
+ let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) }
+
+ # We can not rely on FactoryGirl as the state of Event may change in ways that
+ # the background migration does not expect, hence we use the Event class of
+ # the migration itself.
+ def create_push_event(project, author, data = nil)
+ klass = Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event
+
+ klass.create!(
+ action: klass::PUSHED,
+ project_id: project.id,
+ author_id: author.id,
+ data: data
+ )
+ end
+
+ describe '#perform' do
+ it 'returns if data should not be migrated' do
+ allow(migration).to receive(:migrate?).and_return(false)
+
+ expect(migration).not_to receive(:find_events)
+
+ migration.perform(1, 10)
+ end
+
+ it 'migrates the range of events if data is to be migrated' do
+ event1 = create_push_event(project, author, { commits: [] })
+ event2 = create_push_event(project, author, { commits: [] })
+
+ allow(migration).to receive(:migrate?).and_return(true)
+
+ expect(migration).to receive(:process_event).twice
+
+ migration.perform(event1.id, event2.id)
+ end
+ end
+
+ describe '#process_event' do
+ it 'processes a regular event' do
+ event = double(:event, push_event?: false)
+
+ expect(migration).to receive(:replicate_event)
+ expect(migration).not_to receive(:create_push_event_payload)
+
+ migration.process_event(event)
+ end
+
+ it 'processes a push event' do
+ event = double(:event, push_event?: true)
+
+ expect(migration).to receive(:replicate_event)
+ expect(migration).to receive(:create_push_event_payload)
+
+ migration.process_event(event)
+ end
+ end
+
+ describe '#replicate_event' do
+ it 'replicates the event to the "events_for_migration" table' do
+ event = create_push_event(
+ project,
+ author,
+ data: { commits: [] },
+ title: 'bla'
+ )
+
+ attributes = event
+ .attributes.with_indifferent_access.except(:title, :data)
+
+ expect(described_class::EventForMigration)
+ .to receive(:create!)
+ .with(attributes)
+
+ migration.replicate_event(event)
+ end
+ end
+
+ describe '#create_push_event_payload' do
+ let(:push_data) do
+ {
+ commits: [],
+ ref: 'refs/heads/master',
+ before: '156e0e9adc587a383a7eeb5b21ddecb9044768a8',
+ after: '0' * 40,
+ total_commits_count: 1
+ }
+ end
+
+ let(:event) do
+ create_push_event(project, author, push_data)
+ end
+
+ before do
+ # The foreign key in push_event_payloads at this point points to the
+ # "events_for_migration" table so we need to make sure a row exists in
+ # said table.
+ migration.replicate_event(event)
+ end
+
+ it 'creates a push event payload for an event' do
+ payload = migration.create_push_event_payload(event)
+
+ expect(PushEventPayload.count).to eq(1)
+ expect(payload.valid?).to eq(true)
+ end
+
+ it 'does not create push event payloads for removed events' do
+ allow(event).to receive(:id).and_return(-1)
+
+ payload = migration.create_push_event_payload(event)
+
+ expect(payload).to be_nil
+ expect(PushEventPayload.count).to eq(0)
+ end
+
+ it 'encodes and decodes the commit IDs from and to binary data' do
+ payload = migration.create_push_event_payload(event)
+ packed = migration.pack(push_data[:before])
+
+ expect(payload.commit_from).to eq(packed)
+ expect(payload.commit_to).to be_nil
+ end
+ end
+
+ describe '#find_events' do
+ it 'returns the events for the given ID range' do
+ event1 = create_push_event(project, author, { commits: [] })
+ event2 = create_push_event(project, author, { commits: [] })
+ event3 = create_push_event(project, author, { commits: [] })
+ events = migration.find_events(event1.id, event2.id)
+
+ expect(events.length).to eq(2)
+ expect(events.pluck(:id)).not_to include(event3.id)
+ end
+ end
+
+ describe '#migrate?' do
+ it 'returns true when data should be migrated' do
+ allow(described_class::Event)
+ .to receive(:table_exists?).and_return(true)
+
+ allow(described_class::PushEventPayload)
+ .to receive(:table_exists?).and_return(true)
+
+ allow(described_class::EventForMigration)
+ .to receive(:table_exists?).and_return(true)
+
+ expect(migration.migrate?).to eq(true)
+ end
+
+ it 'returns false if the "events" table does not exist' do
+ allow(described_class::Event)
+ .to receive(:table_exists?).and_return(false)
+
+ expect(migration.migrate?).to eq(false)
+ end
+
+ it 'returns false if the "push_event_payloads" table does not exist' do
+ allow(described_class::Event)
+ .to receive(:table_exists?).and_return(true)
+
+ allow(described_class::PushEventPayload)
+ .to receive(:table_exists?).and_return(false)
+
+ expect(migration.migrate?).to eq(false)
+ end
+
+ it 'returns false when the "events_for_migration" table does not exist' do
+ allow(described_class::Event)
+ .to receive(:table_exists?).and_return(true)
+
+ allow(described_class::PushEventPayload)
+ .to receive(:table_exists?).and_return(true)
+
+ allow(described_class::EventForMigration)
+ .to receive(:table_exists?).and_return(false)
+
+ expect(migration.migrate?).to eq(false)
+ end
+ end
+
+ describe '#pack' do
+ it 'packs a SHA1 into a 20 byte binary string' do
+ packed = migration.pack('156e0e9adc587a383a7eeb5b21ddecb9044768a8')
+
+ expect(packed.bytesize).to eq(20)
+ end
+
+ it 'returns nil if the input value is nil' do
+ expect(migration.pack(nil)).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/migrate_stage_status_spec.rb b/spec/lib/gitlab/background_migration/migrate_stage_status_spec.rb
new file mode 100644
index 00000000000..878158910be
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_stage_status_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::MigrateStageStatus, :migration, schema: 20170711145320 do
+ let(:projects) { table(:projects) }
+ let(:pipelines) { table(:ci_pipelines) }
+ let(:stages) { table(:ci_stages) }
+ let(:jobs) { table(:ci_builds) }
+
+ STATUSES = { created: 0, pending: 1, running: 2, success: 3,
+ failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
+
+ before do
+ projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1')
+ pipelines.create!(id: 1, project_id: 1, ref: 'master', sha: 'adf43c3a')
+ stages.create!(id: 1, pipeline_id: 1, project_id: 1, name: 'test', status: nil)
+ stages.create!(id: 2, pipeline_id: 1, project_id: 1, name: 'deploy', status: nil)
+ end
+
+ context 'when stage status is known' do
+ before do
+ create_job(project: 1, pipeline: 1, stage: 'test', status: 'success')
+ create_job(project: 1, pipeline: 1, stage: 'test', status: 'running')
+ create_job(project: 1, pipeline: 1, stage: 'deploy', status: 'failed')
+ end
+
+ it 'sets a correct stage status' do
+ described_class.new.perform(1, 2)
+
+ expect(stages.first.status).to eq STATUSES[:running]
+ expect(stages.second.status).to eq STATUSES[:failed]
+ end
+ end
+
+ context 'when stage status is not known' do
+ it 'sets a skipped stage status' do
+ described_class.new.perform(1, 2)
+
+ expect(stages.first.status).to eq STATUSES[:skipped]
+ expect(stages.second.status).to eq STATUSES[:skipped]
+ end
+ end
+
+ context 'when stage status includes status of a retried job' do
+ before do
+ create_job(project: 1, pipeline: 1, stage: 'test', status: 'canceled')
+ create_job(project: 1, pipeline: 1, stage: 'deploy', status: 'failed', retried: true)
+ create_job(project: 1, pipeline: 1, stage: 'deploy', status: 'success')
+ end
+
+ it 'sets a correct stage status' do
+ described_class.new.perform(1, 2)
+
+ expect(stages.first.status).to eq STATUSES[:canceled]
+ expect(stages.second.status).to eq STATUSES[:success]
+ end
+ end
+
+ context 'when some job in the stage is blocked / manual' do
+ before do
+ create_job(project: 1, pipeline: 1, stage: 'test', status: 'failed')
+ create_job(project: 1, pipeline: 1, stage: 'test', status: 'manual')
+ create_job(project: 1, pipeline: 1, stage: 'deploy', status: 'success', when: 'manual')
+ end
+
+ it 'sets a correct stage status' do
+ described_class.new.perform(1, 2)
+
+ expect(stages.first.status).to eq STATUSES[:manual]
+ expect(stages.second.status).to eq STATUSES[:success]
+ end
+ end
+
+ def create_job(project:, pipeline:, stage:, status:, **opts)
+ stages = { test: 1, build: 2, deploy: 3 }
+
+ jobs.create!(project_id: project, commit_id: pipeline,
+ stage_idx: stages[stage.to_sym], stage: stage,
+ status: status, **opts)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb
new file mode 100644
index 00000000000..ee60e498b59
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do
+ let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'move_snippet_files_test') }
+ let(:old_uploads_dir) { File.join('uploads', 'system', 'personal_snippet') }
+ let(:new_uploads_dir) { File.join('uploads', '-', 'system', 'personal_snippet') }
+ let(:snippet) do
+ snippet = create(:personal_snippet)
+ create_upload_for_snippet(snippet)
+ snippet.update_attributes!(description: markdown_linking_file(snippet))
+ snippet
+ end
+
+ let(:migration) { described_class.new }
+
+ before do
+ allow(migration).to receive(:base_directory) { test_dir }
+ end
+
+ describe '#perform' do
+ it 'moves the file on the disk' do
+ expected_path = File.join(test_dir, new_uploads_dir, snippet.id.to_s, "secret#{snippet.id}", 'upload.txt')
+
+ migration.perform(old_uploads_dir, new_uploads_dir)
+
+ expect(File.exist?(expected_path)).to be_truthy
+ end
+
+ it 'updates the markdown of the snippet' do
+ expected_path = File.join(new_uploads_dir, snippet.id.to_s, "secret#{snippet.id}", 'upload.txt')
+ expected_markdown = "[an upload](#{expected_path})"
+
+ migration.perform(old_uploads_dir, new_uploads_dir)
+
+ expect(snippet.reload.description).to eq(expected_markdown)
+ end
+
+ it 'updates the markdown of notes' do
+ expected_path = File.join(new_uploads_dir, snippet.id.to_s, "secret#{snippet.id}", 'upload.txt')
+ expected_markdown = "with [an upload](#{expected_path})"
+
+ note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown_linking_file(snippet)}")
+
+ migration.perform(old_uploads_dir, new_uploads_dir)
+
+ expect(note.reload.note).to eq(expected_markdown)
+ end
+ end
+
+ def create_upload_for_snippet(snippet)
+ snippet_path = path_for_file_in_snippet(snippet)
+ path = File.join(old_uploads_dir, snippet.id.to_s, snippet_path)
+ absolute_path = File.join(test_dir, path)
+
+ FileUtils.mkdir_p(File.dirname(absolute_path))
+ FileUtils.touch(absolute_path)
+
+ create(:upload, model: snippet, path: snippet_path, uploader: PersonalFileUploader)
+ end
+
+ def path_for_file_in_snippet(snippet)
+ secret = "secret#{snippet.id}"
+ filename = 'upload.txt'
+
+ File.join(secret, filename)
+ end
+
+ def markdown_linking_file(snippet)
+ path = File.join(old_uploads_dir, snippet.id.to_s, path_for_file_in_snippet(snippet))
+ "[an upload](#{path})"
+ end
+end
diff --git a/spec/lib/gitlab/bare_repository_importer_spec.rb b/spec/lib/gitlab/bare_repository_importer_spec.rb
new file mode 100644
index 00000000000..36d1844b5b1
--- /dev/null
+++ b/spec/lib/gitlab/bare_repository_importer_spec.rb
@@ -0,0 +1,100 @@
+require 'spec_helper'
+
+describe Gitlab::BareRepositoryImporter, repository: true do
+ subject(:importer) { described_class.new('default', project_path) }
+
+ let!(:admin) { create(:admin) }
+
+ before do
+ allow(described_class).to receive(:log)
+ end
+
+ shared_examples 'importing a repository' do
+ describe '.execute' do
+ it 'creates a project for a repository in storage' do
+ FileUtils.mkdir_p(File.join(TestEnv.repos_path, "#{project_path}.git"))
+ fake_importer = double
+
+ expect(described_class).to receive(:new).with('default', project_path)
+ .and_return(fake_importer)
+ expect(fake_importer).to receive(:create_project_if_needed)
+
+ described_class.execute
+ end
+
+ it 'skips wiki repos' do
+ FileUtils.mkdir_p(File.join(TestEnv.repos_path, 'the-group', 'the-project.wiki.git'))
+
+ expect(described_class).to receive(:log).with(' * Skipping wiki repo')
+ expect(described_class).not_to receive(:new)
+
+ described_class.execute
+ end
+ end
+
+ describe '#initialize' do
+ context 'without admin users' do
+ let(:admin) { nil }
+
+ it 'raises an error' do
+ expect { importer }.to raise_error(Gitlab::BareRepositoryImporter::NoAdminError)
+ end
+ end
+ end
+
+ describe '#create_project_if_needed' do
+ it 'starts an import for a project that did not exist' do
+ expect(importer).to receive(:create_project)
+
+ importer.create_project_if_needed
+ end
+
+ it 'skips importing when the project already exists' do
+ project = create(:project, path: 'a-project', namespace: existing_group)
+
+ expect(importer).not_to receive(:create_project)
+ expect(importer).to receive(:log).with(" * #{project.name} (#{project_path}) exists")
+
+ importer.create_project_if_needed
+ end
+
+ it 'creates a project with the correct path in the database' do
+ importer.create_project_if_needed
+
+ expect(Project.find_by_full_path(project_path)).not_to be_nil
+ end
+ end
+ end
+
+ context 'with subgroups', :nested_groups do
+ let(:project_path) { 'a-group/a-sub-group/a-project' }
+
+ let(:existing_group) do
+ group = create(:group, path: 'a-group')
+ create(:group, path: 'a-sub-group', parent: group)
+ end
+
+ it_behaves_like 'importing a repository'
+ end
+
+ context 'without subgroups' do
+ let(:project_path) { 'a-group/a-project' }
+ let(:existing_group) { create(:group, path: 'a-group') }
+
+ it_behaves_like 'importing a repository'
+ end
+
+ context 'when subgroups are not available' do
+ let(:project_path) { 'a-group/a-sub-group/a-project' }
+
+ before do
+ expect(Group).to receive(:supports_nested_groups?) { false }
+ end
+
+ describe '#create_project_if_needed' do
+ it 'raises an error' do
+ expect { importer.create_project_if_needed }.to raise_error('Nested groups are not supported on MySQL')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
index d7f7b26740d..ae5b31dc12d 100644
--- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::BitbucketImport::ProjectCreator do
has_wiki?: false)
end
- let(:namespace){ create(:group, owner: user) }
+ let(:namespace) { create(:group, owner: user) }
let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" }
let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } }
diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb
index 6c4cfa1203e..f8c8b83a3ac 100644
--- a/spec/lib/gitlab/checks/force_push_spec.rb
+++ b/spec/lib/gitlab/checks/force_push_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Checks::ForcePush do
let(:project) { create(:project, :repository) }
- context "exit code checking" do
+ context "exit code checking", skip_gitaly_mock: true 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])
diff --git a/spec/lib/gitlab/ci/build/artifacts/path_spec.rb b/spec/lib/gitlab/ci/build/artifacts/path_spec.rb
new file mode 100644
index 00000000000..7bd6a2ead25
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/artifacts/path_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Artifacts::Path do
+ describe '#valid?' do
+ context 'when path contains a zero character' do
+ it 'is not valid' do
+ expect(described_class.new("something/\255")).not_to be_valid
+ end
+ end
+
+ context 'when path is not utf8 string' do
+ it 'is not valid' do
+ expect(described_class.new("something/\0")).not_to be_valid
+ end
+ end
+
+ context 'when path is valid' do
+ it 'is valid' do
+ expect(described_class.new("some/file/path")).to be_valid
+ end
+ end
+ end
+
+ describe '#directory?' do
+ context 'when path ends with a directory indicator' do
+ it 'is a directory' do
+ expect(described_class.new("some/file/dir/")).to be_directory
+ end
+ end
+
+ context 'when path does not end with a directory indicator' do
+ it 'is not a directory' do
+ expect(described_class.new("some/file")).not_to be_directory
+ end
+ end
+ end
+
+ describe '#name' do
+ it 'returns a base name' do
+ expect(described_class.new("some/file").name).to eq 'file'
+ end
+ end
+
+ describe '#nodes' do
+ it 'returns number of path nodes' do
+ expect(described_class.new("some/dir/file").nodes).to eq 2
+ end
+ end
+
+ describe '#to_s' do
+ context 'when path is valid' do
+ it 'returns a string representation of a path' do
+ expect(described_class.new('some/path').to_s).to eq 'some/path'
+ end
+ end
+
+ context 'when path is invalid' do
+ it 'raises an error' do
+ expect { described_class.new("invalid/\0").to_s }
+ .to raise_error ArgumentError
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb b/spec/lib/gitlab/ci/config/entry/attributable_spec.rb
index fde03c51e2c..b028b771375 100644
--- a/spec/lib/gitlab/ci/config/entry/attributable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/attributable_spec.rb
@@ -1,18 +1,21 @@
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Attributable do
- let(:node) { Class.new }
+ let(:node) do
+ Class.new do
+ include Gitlab::Ci::Config::Entry::Attributable
+ end
+ end
+
let(:instance) { node.new }
before do
- node.include(described_class)
-
node.class_eval do
attributes :name, :test
end
end
- context 'config is a hash' do
+ context 'when config is a hash' do
before do
allow(instance)
.to receive(:config)
@@ -29,7 +32,7 @@ describe Gitlab::Ci::Config::Entry::Attributable do
end
end
- context 'config is not a hash' do
+ context 'when config is not a hash' do
before do
allow(instance)
.to receive(:config)
@@ -40,4 +43,18 @@ describe Gitlab::Ci::Config::Entry::Attributable do
expect(instance.test).to be_nil
end
end
+
+ context 'when method is already defined in a superclass' do
+ it 'raises an error' do
+ expectation = expect do
+ Class.new(String) do
+ include Gitlab::Ci::Config::Entry::Attributable
+
+ attributes :length
+ end
+ end
+
+ expectation.to raise_error(ArgumentError, 'Method already defined!')
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb b/spec/lib/gitlab/ci/config/entry/configurable_spec.rb
index ae7e628b5b5..088d4b472da 100644
--- a/spec/lib/gitlab/ci/config/entry/configurable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/configurable_spec.rb
@@ -1,40 +1,26 @@
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Configurable do
- let(:entry) { Class.new }
-
- before do
- entry.include(described_class)
+ let(:entry) do
+ Class.new(Gitlab::Ci::Config::Entry::Node) do
+ include Gitlab::Ci::Config::Entry::Configurable
+ end
end
describe 'validations' do
- let(:validator) { entry.validator.new(instance) }
-
- before do
- entry.class_eval do
- attr_reader :config
+ context 'when entry is a hash' do
+ let(:instance) { entry.new(key: 'value') }
- def initialize(config)
- @config = config
- end
+ it 'correctly validates an instance' do
+ expect(instance).to be_valid
end
-
- validator.validate
end
- context 'when entry validator is invalid' do
+ context 'when entry is not a hash' do
let(:instance) { entry.new('ls') }
- it 'returns invalid validator' do
- expect(validator).to be_invalid
- end
- end
-
- context 'when entry instance is valid' do
- let(:instance) { entry.new(key: 'value') }
-
- it 'returns valid validator' do
- expect(validator).to be_valid
+ it 'invalidates the instance' do
+ expect(instance).not_to be_valid
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
new file mode 100644
index 00000000000..5e83abf645b
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Policy do
+ let(:entry) { described_class.new(config) }
+
+ context 'when using simplified policy' do
+ describe 'validations' do
+ context 'when entry config value is valid' do
+ context 'when config is a branch or tag name' do
+ let(:config) { %w[master feature/branch] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns refs hash' do
+ expect(entry.value).to eq(refs: config)
+ end
+ end
+ end
+
+ context 'when config is a regexp' do
+ let(:config) { ['/^issue-.*$/'] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when config is a special keyword' do
+ let(:config) { %w[tags triggers branches] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+ end
+
+ context 'when entry value is not valid' do
+ let(:config) { [1] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include /policy config should be an array of strings or regexps/
+ end
+ end
+ end
+ end
+ end
+
+ context 'when using complex policy' do
+ context 'when specifiying refs policy' do
+ let(:config) { { refs: ['master'] } }
+
+ it 'is a correct configuraton' do
+ expect(entry).to be_valid
+ expect(entry.value).to eq(refs: %w[master])
+ end
+ end
+
+ context 'when specifying kubernetes policy' do
+ let(:config) { { kubernetes: 'active' } }
+
+ it 'is a correct configuraton' do
+ expect(entry).to be_valid
+ expect(entry.value).to eq(kubernetes: 'active')
+ end
+ end
+
+ context 'when specifying invalid kubernetes policy' do
+ let(:config) { { kubernetes: 'something' } }
+
+ it 'reports an error about invalid policy' do
+ expect(entry.errors).to include /unknown value: something/
+ end
+ end
+
+ context 'when specifying unknown policy' do
+ let(:config) { { refs: ['master'], invalid: :something } }
+
+ it 'returns error about invalid key' do
+ expect(entry.errors).to include /unknown keys: invalid/
+ end
+ end
+
+ context 'when policy is empty' do
+ let(:config) { {} }
+
+ it 'is not a valid configuration' do
+ expect(entry.errors).to include /can't be blank/
+ end
+ end
+ end
+
+ context 'when policy strategy does not match' do
+ let(:config) { 'string strategy' }
+
+ it 'returns information about errors' do
+ expect(entry.errors)
+ .to include /has to be either an array of conditions or a hash/
+ end
+ end
+
+ describe '.default' do
+ it 'does not have a default value' do
+ expect(described_class.default).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb b/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb
new file mode 100644
index 00000000000..395062207a3
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/simplifiable_spec.rb
@@ -0,0 +1,88 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Simplifiable do
+ describe '.strategy' do
+ let(:entry) do
+ Class.new(described_class) do
+ strategy :Something, if: -> { 'condition' }
+ strategy :DifferentOne, if: -> { 'condition' }
+ end
+ end
+
+ it 'defines entry strategies' do
+ expect(entry.strategies.size).to eq 2
+ expect(entry.strategies.map(&:name))
+ .to eq %i[Something DifferentOne]
+ end
+ end
+
+ describe 'setting strategy by a condition' do
+ let(:first) { double('first strategy') }
+ let(:second) { double('second strategy') }
+ let(:unknown) { double('unknown strategy') }
+
+ before do
+ stub_const("#{described_class.name}::Something", first)
+ stub_const("#{described_class.name}::DifferentOne", second)
+ stub_const("#{described_class.name}::UnknownStrategy", unknown)
+ end
+
+ context 'when first strategy should be used' do
+ let(:entry) do
+ Class.new(described_class) do
+ strategy :Something, if: -> (arg) { arg == 'something' }
+ strategy :DifferentOne, if: -> (*) { false }
+ end
+ end
+
+ it 'attemps to load a first strategy' do
+ expect(first).to receive(:new).with('something', anything)
+
+ entry.new('something')
+ end
+ end
+
+ context 'when second strategy should be used' do
+ let(:entry) do
+ Class.new(described_class) do
+ strategy :Something, if: -> (arg) { arg == 'something' }
+ strategy :DifferentOne, if: -> (arg) { arg == 'test' }
+ end
+ end
+
+ it 'attemps to load a second strategy' do
+ expect(second).to receive(:new).with('test', anything)
+
+ entry.new('test')
+ end
+ end
+
+ context 'when neither one is a valid strategy' do
+ let(:entry) do
+ Class.new(described_class) do
+ strategy :Something, if: -> (*) { false }
+ strategy :DifferentOne, if: -> (*) { false }
+ end
+ end
+
+ it 'instantiates an unknown strategy' do
+ expect(unknown).to receive(:new).with('test', anything)
+
+ entry.new('test')
+ end
+ end
+ end
+
+ context 'when a unknown strategy class is not defined' do
+ let(:entry) do
+ Class.new(described_class) do
+ strategy :String, if: -> (*) { true }
+ end
+ end
+
+ it 'raises an error when being initialized' do
+ expect { entry.new('something') }
+ .to raise_error ArgumentError, /UndefinedStrategy not available!/
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
deleted file mode 100644
index e4ee44f1274..00000000000
--- a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Entry::Trigger do
- let(:entry) { described_class.new(config) }
-
- describe 'validations' do
- context 'when entry config value is valid' do
- context 'when config is a branch or tag name' do
- let(:config) { %w[master feature/branch] }
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
-
- describe '#value' do
- it 'returns key value' do
- expect(entry.value).to eq config
- end
- end
- end
-
- context 'when config is a regexp' do
- let(:config) { ['/^issue-.*$/'] }
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
-
- context 'when config is a special keyword' do
- let(:config) { %w[tags triggers branches] }
-
- describe '#valid?' do
- it 'is valid' do
- expect(entry).to be_valid
- end
- end
- end
- end
-
- context 'when entry value is not valid' do
- let(:config) { [1] }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'trigger config should be an array of strings or regexps'
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb b/spec/lib/gitlab/ci/config/entry/validatable_spec.rb
index d1856801827..ae2a7a51ba6 100644
--- a/spec/lib/gitlab/ci/config/entry/validatable_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/validatable_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
describe Gitlab::Ci::Config::Entry::Validatable do
- let(:entry) { Class.new }
-
- before do
- entry.include(described_class)
+ let(:entry) do
+ Class.new(Gitlab::Ci::Config::Entry::Node) do
+ include Gitlab::Ci::Config::Entry::Validatable
+ end
end
describe '.validator' do
@@ -28,7 +28,7 @@ describe Gitlab::Ci::Config::Entry::Validatable do
end
context 'when validating entry instance' do
- let(:entry_instance) { entry.new }
+ let(:entry_instance) { entry.new('something') }
context 'when attribute is valid' do
before do
diff --git a/spec/lib/gitlab/ci/config/entry/validator_spec.rb b/spec/lib/gitlab/ci/config/entry/validator_spec.rb
index ad7e6f07d3c..172b6b47a4f 100644
--- a/spec/lib/gitlab/ci/config/entry/validator_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/validator_spec.rb
@@ -48,7 +48,7 @@ describe Gitlab::Ci::Config::Entry::Validator do
validator_instance.validate
expect(validator_instance.messages)
- .to include "node test attribute can't be blank"
+ .to include /test attribute can't be blank/
end
end
end
diff --git a/spec/lib/gitlab/ci/stage/seed_spec.rb b/spec/lib/gitlab/ci/stage/seed_spec.rb
index d7e91a5a62c..9ecd128faca 100644
--- a/spec/lib/gitlab/ci/stage/seed_spec.rb
+++ b/spec/lib/gitlab/ci/stage/seed_spec.rb
@@ -27,6 +27,26 @@ describe Gitlab::Ci::Stage::Seed do
expect(subject.builds)
.to all(include(trigger_request: pipeline.trigger_requests.first))
end
+
+ context 'when a ref is protected' do
+ before do
+ allow_any_instance_of(Project).to receive(:protected_for?).and_return(true)
+ end
+
+ it 'returns protected builds' do
+ expect(subject.builds).to all(include(protected: true))
+ end
+ end
+
+ context 'when a ref is unprotected' do
+ before do
+ allow_any_instance_of(Project).to receive(:protected_for?).and_return(false)
+ end
+
+ it 'returns unprotected builds' do
+ expect(subject.builds).to all(include(protected: false))
+ end
+ end
end
describe '#user=' do
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index ebe5af56160..e5555546fa8 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -295,7 +295,7 @@ describe Gitlab::Ci::Trace::Stream do
end
context 'malicious regexp' do
- let(:data) { malicious_text }
+ let(:data) { malicious_text }
let(:regex) { malicious_regexp }
include_examples 'malicious regexp'
diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
index 854aaa34c73..0560c47f03f 100644
--- a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
@@ -6,10 +6,10 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do
let(:user) { create(:user, :admin) }
let(:start_time_attrs) { Issue.arel_table[:created_at] }
let(:end_time_attrs) { [Issue::Metrics.arel_table[:first_associated_with_milestone_at]] }
- let(:options) do
+ let(:options) do
{ start_time_attrs: start_time_attrs,
end_time_attrs: end_time_attrs,
- from: 30.days.ago }
+ from: 30.days.ago }
end
subject do
diff --git a/spec/lib/gitlab/daemon_spec.rb b/spec/lib/gitlab/daemon_spec.rb
index c519984a267..2bdb0dfc736 100644
--- a/spec/lib/gitlab/daemon_spec.rb
+++ b/spec/lib/gitlab/daemon_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::Daemon do
allow(Kernel).to receive(:at_exit)
end
- after(:each) do
+ after do
described_class.instance_variable_set(:@instance, nil)
end
diff --git a/spec/lib/gitlab/data_builder/note_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb
index 6415e4083d6..aaa42566a4d 100644
--- a/spec/lib/gitlab/data_builder/note_spec.rb
+++ b/spec/lib/gitlab/data_builder/note_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::DataBuilder::Note do
let(:data) { described_class.build(note, user) }
let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors
- before(:each) do
+ before do
expect(data).to have_key(:object_attributes)
expect(data[:object_attributes]).to have_key(:url)
expect(data[:object_attributes][:url])
diff --git a/spec/lib/gitlab/database/grant_spec.rb b/spec/lib/gitlab/database/grant_spec.rb
new file mode 100644
index 00000000000..651da3e8476
--- /dev/null
+++ b/spec/lib/gitlab/database/grant_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::Database::Grant do
+ describe '.scope_to_current_user' do
+ it 'scopes the relation to the current user' do
+ user = Gitlab::Database.username
+ column = Gitlab::Database.postgresql? ? :grantee : :User
+ names = described_class.scope_to_current_user.pluck(column).uniq
+
+ expect(names).to eq([user])
+ end
+ end
+
+ describe '.create_and_execute_trigger' do
+ it 'returns true when the user can create and execute a trigger' do
+ # We assume the DB/user is set up correctly so that triggers can be
+ # created, which is necessary anyway for other tests to work.
+ expect(described_class.create_and_execute_trigger?('users')).to eq(true)
+ end
+
+ it 'returns false when the user can not create and/or execute a trigger' do
+ allow(described_class).to receive(:scope_to_current_user)
+ .and_return(described_class.none)
+
+ result = described_class.create_and_execute_trigger?('kittens')
+
+ expect(result).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index ec2274a70aa..1bcdc369c44 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -2,9 +2,7 @@ require 'spec_helper'
describe Gitlab::Database::MigrationHelpers do
let(:model) do
- ActiveRecord::Migration.new.extend(
- described_class
- )
+ ActiveRecord::Migration.new.extend(described_class)
end
before do
@@ -452,6 +450,8 @@ describe Gitlab::Database::MigrationHelpers do
it 'renames a column concurrently' do
allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ expect(model).to receive(:check_trigger_permissions!).with(:users)
+
expect(model).to receive(:install_rename_triggers_for_mysql)
.with(trigger_name, 'users', 'old', 'new')
@@ -479,6 +479,8 @@ describe Gitlab::Database::MigrationHelpers do
it 'renames a column concurrently' do
allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ expect(model).to receive(:check_trigger_permissions!).with(:users)
+
expect(model).to receive(:install_rename_triggers_for_postgresql)
.with(trigger_name, 'users', 'old', 'new')
@@ -508,6 +510,8 @@ describe Gitlab::Database::MigrationHelpers do
it 'cleans up the renaming procedure for PostgreSQL' do
allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ expect(model).to receive(:check_trigger_permissions!).with(:users)
+
expect(model).to receive(:remove_rename_triggers_for_postgresql)
.with(:users, /trigger_.{12}/)
@@ -519,6 +523,8 @@ describe Gitlab::Database::MigrationHelpers do
it 'cleans up the renaming procedure for MySQL' do
allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+ expect(model).to receive(:check_trigger_permissions!).with(:users)
+
expect(model).to receive(:remove_rename_triggers_for_mysql)
.with(/trigger_.{12}/)
@@ -575,8 +581,8 @@ describe Gitlab::Database::MigrationHelpers do
describe '#remove_rename_triggers_for_postgresql' do
it 'removes the function and trigger' do
- expect(model).to receive(:execute).with('DROP TRIGGER foo ON bar')
- expect(model).to receive(:execute).with('DROP FUNCTION foo()')
+ expect(model).to receive(:execute).with('DROP TRIGGER IF EXISTS foo ON bar')
+ expect(model).to receive(:execute).with('DROP FUNCTION IF EXISTS foo()')
model.remove_rename_triggers_for_postgresql('bar', 'foo')
end
@@ -584,8 +590,8 @@ describe Gitlab::Database::MigrationHelpers do
describe '#remove_rename_triggers_for_mysql' do
it 'removes the triggers' do
- expect(model).to receive(:execute).with('DROP TRIGGER foo_insert')
- expect(model).to receive(:execute).with('DROP TRIGGER foo_update')
+ expect(model).to receive(:execute).with('DROP TRIGGER IF EXISTS foo_insert')
+ expect(model).to receive(:execute).with('DROP TRIGGER IF EXISTS foo_update')
model.remove_rename_triggers_for_mysql('foo')
end
@@ -845,4 +851,67 @@ describe Gitlab::Database::MigrationHelpers do
end
end
end
+
+ describe 'sidekiq migration helpers', :sidekiq, :redis do
+ let(:worker) do
+ Class.new do
+ include Sidekiq::Worker
+ sidekiq_options queue: 'test'
+ end
+ end
+
+ describe '#sidekiq_queue_length' do
+ context 'when queue is empty' do
+ it 'returns zero' do
+ Sidekiq::Testing.disable! do
+ expect(model.sidekiq_queue_length('test')).to eq 0
+ end
+ end
+ end
+
+ context 'when queue contains jobs' do
+ it 'returns correct size of the queue' do
+ Sidekiq::Testing.disable! do
+ worker.perform_async('Something', [1])
+ worker.perform_async('Something', [2])
+
+ expect(model.sidekiq_queue_length('test')).to eq 2
+ end
+ end
+ end
+ end
+
+ describe '#migrate_sidekiq_queue' do
+ it 'migrates jobs from one sidekiq queue to another' do
+ Sidekiq::Testing.disable! do
+ worker.perform_async('Something', [1])
+ worker.perform_async('Something', [2])
+
+ expect(model.sidekiq_queue_length('test')).to eq 2
+ expect(model.sidekiq_queue_length('new_test')).to eq 0
+
+ model.sidekiq_queue_migrate('test', to: 'new_test')
+
+ expect(model.sidekiq_queue_length('test')).to eq 0
+ expect(model.sidekiq_queue_length('new_test')).to eq 2
+ end
+ end
+ end
+ end
+
+ describe '#check_trigger_permissions!' do
+ it 'does nothing when the user has the correct permissions' do
+ expect { model.check_trigger_permissions!('users') }
+ .not_to raise_error(RuntimeError)
+ end
+
+ it 'raises RuntimeError when the user does not have the correct permissions' do
+ allow(Gitlab::Database::Grant).to receive(:create_and_execute_trigger?)
+ .with('kittens')
+ .and_return(false)
+
+ expect { model.check_trigger_permissions!('kittens') }
+ .to raise_error(RuntimeError, /Your database user is not allowed/)
+ end
+ end
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index c5f9aecd867..5fa94999d25 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -51,6 +51,28 @@ describe Gitlab::Database do
end
end
+ describe '.join_lateral_supported?' do
+ it 'returns false when using MySQL' do
+ allow(described_class).to receive(:postgresql?).and_return(false)
+
+ expect(described_class.join_lateral_supported?).to eq(false)
+ end
+
+ it 'returns false when using PostgreSQL 9.2' do
+ allow(described_class).to receive(:postgresql?).and_return(true)
+ allow(described_class).to receive(:version).and_return('9.2.1')
+
+ expect(described_class.join_lateral_supported?).to eq(false)
+ end
+
+ it 'returns true when using PostgreSQL 9.3.0 or newer' do
+ allow(described_class).to receive(:postgresql?).and_return(true)
+ allow(described_class).to receive(:version).and_return('9.3.0')
+
+ expect(described_class.join_lateral_supported?).to eq(true)
+ end
+ end
+
describe '.nulls_last_order' do
context 'when using PostgreSQL' do
before do
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index d3d841b0668..c91895cedc3 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -15,6 +15,17 @@ describe Gitlab::Diff::File do
it { expect(diff_lines.first).to be_kind_of(Gitlab::Diff::Line) }
end
+ describe '#highlighted_diff_lines' do
+ it 'highlights the diff and memoises the result' do
+ expect(Gitlab::Diff::Highlight).to receive(:new)
+ .with(diff_file, repository: project.repository)
+ .once
+ .and_call_original
+
+ diff_file.highlighted_diff_lines
+ end
+ end
+
describe '#mode_changed?' do
it { expect(diff_file.mode_changed?).to be_falsey }
end
@@ -122,8 +133,20 @@ describe Gitlab::Diff::File do
let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') }
- it 'returns true' do
- expect(diff_file.content_changed?).to be_truthy
+ context 'when the blobs are different' do
+ it 'returns true' do
+ expect(diff_file.content_changed?).to be_truthy
+ end
+ end
+
+ context 'when there are no diff refs' do
+ before do
+ allow(diff_file).to receive(:diff_refs).and_return(nil)
+ end
+
+ it 'returns false' do
+ expect(diff_file.content_changed?).to be_falsey
+ end
end
end
@@ -131,8 +154,20 @@ describe Gitlab::Diff::File do
let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') }
- it 'returns true' do
- expect(diff_file.content_changed?).to be_truthy
+ context 'when the blobs are different' do
+ it 'returns true' do
+ expect(diff_file.content_changed?).to be_truthy
+ end
+ end
+
+ context 'when there are no diff refs' do
+ before do
+ allow(diff_file).to receive(:diff_refs).and_return(nil)
+ end
+
+ it 'returns true' do
+ expect(diff_file.content_changed?).to be_truthy
+ end
end
end
end
@@ -270,6 +305,21 @@ describe Gitlab::Diff::File do
expect(diff_file.simple_viewer).to be_a(DiffViewer::ModeChanged)
end
end
+
+ context 'when no other conditions apply' do
+ before do
+ allow(diff_file).to receive(:content_changed?).and_return(false)
+ allow(diff_file).to receive(:new_file?).and_return(false)
+ allow(diff_file).to receive(:deleted_file?).and_return(false)
+ allow(diff_file).to receive(:renamed_file?).and_return(false)
+ allow(diff_file).to receive(:mode_changed?).and_return(false)
+ allow(diff_file).to receive(:raw_text?).and_return(false)
+ end
+
+ it 'returns a No Preview viewer' do
+ expect(diff_file.simple_viewer).to be_a(DiffViewer::NoPreview)
+ end
+ end
end
describe '#rich_viewer' do
diff --git a/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb
index 046b096e366..7e17437fa2a 100644
--- a/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_markdown_marker_spec.rb
@@ -6,9 +6,9 @@ describe Gitlab::Diff::InlineDiffMarkdownMarker do
let(:inline_diffs) { [2..5] }
let(:subject) { described_class.new(raw).mark(inline_diffs, mode: :deletion) }
- it 'marks the range' do
- expect(subject).to eq("ab{-c &#39;d-}ef&#39;")
- expect(subject).to be_html_safe
+ it 'does not escape html etities and marks the range' do
+ expect(subject).to eq("ab{-c 'd-}ef'")
+ expect(subject).not_to be_html_safe
end
end
end
diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
index c3bf34c24ae..7296bbf5df3 100644
--- a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb
@@ -2,11 +2,13 @@ require 'spec_helper'
describe Gitlab::Diff::InlineDiffMarker do
describe '#mark' do
+ let(:inline_diffs) { [2..5] }
+ let(:raw) { "abc 'def'" }
+
+ subject { described_class.new(raw, rich).mark(inline_diffs) }
+
context "when the rich text is html safe" do
- let(:raw) { "abc 'def'" }
let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">&#39;def&#39;</span>}.html_safe }
- let(:inline_diffs) { [2..5] }
- let(:subject) { described_class.new(raw, rich).mark(inline_diffs) }
it 'marks the range' do
expect(subject).to eq(%{<span class="abc">ab<span class="idiff left">c</span></span><span class="space"><span class="idiff"> </span></span><span class="def"><span class="idiff right">&#39;d</span>ef&#39;</span>})
@@ -15,12 +17,10 @@ describe Gitlab::Diff::InlineDiffMarker do
end
context "when the text text is not html safe" do
- let(:raw) { "abc 'def'" }
- let(:inline_diffs) { [2..5] }
- let(:subject) { described_class.new(raw).mark(inline_diffs) }
+ let(:rich) { "abc 'def' differs" }
it 'marks the range' do
- expect(subject).to eq(%{ab<span class="idiff left right">c &#39;d</span>ef&#39;})
+ expect(subject).to eq(%{ab<span class="idiff left right">c &#39;d</span>ef&#39; differs})
expect(subject).to be_html_safe
end
end
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
index bd36d1d309d..6568a0b1bb0 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::Email::Handler::CreateIssueHandler do
let(:email_raw) { fixture_file('emails/valid_new_issue.eml') }
let(:namespace) { create(:namespace, path: 'gitlabhq') }
- let!(:project) { create(:project, :public, :repository, namespace: namespace) }
+ let!(:project) { create(:project, :public, namespace: namespace, path: 'gitlabhq') }
let!(:user) do
create(
:user,
diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb
index 83c4d177cae..0ec1f931037 100644
--- a/spec/lib/gitlab/email/message/repository_push_spec.rb
+++ b/spec/lib/gitlab/email/message/repository_push_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Email::Message::RepositoryPush do
include RepoHelpers
let!(:group) { create(:group, name: 'my_group') }
- let!(:project) { create(:project, :repository, name: 'my_project', namespace: group) }
+ let!(:project) { create(:project, :repository, namespace: group) }
let!(:author) { create(:author, name: 'Author') }
let(:message) do
@@ -38,7 +38,7 @@ describe Gitlab::Email::Message::RepositoryPush do
describe '#project_name_with_namespace' do
subject { message.project_name_with_namespace }
- it { is_expected.to eq 'my_group / my_project' }
+ it { is_expected.to eq "#{group.name} / #{project.path}" }
end
describe '#author' do
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index 1482ef7132d..8b14b227e65 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -30,6 +30,53 @@ describe Gitlab::EncodingHelper do
it 'leaves binary string as is' do
expect(ext_class.encode!(binary_string)).to eq(binary_string)
end
+
+ context 'with corrupted diff' do
+ let(:corrupted_diff) do
+ with_empty_bare_repository do |repo|
+ content = File.read(Rails.root.join(
+ 'spec/fixtures/encoding/Japanese.md').to_s)
+ commit_a = commit(repo, 'Japanese.md', content)
+ commit_b = commit(repo, 'Japanese.md',
+ content.sub('[TODO: Link]', '[ç¾åœ¨ä½œæ¥­ä¸­ã§ã™: Link]'))
+
+ repo.diff(commit_a, commit_b).each_line.map(&:content).join
+ end
+ end
+
+ let(:cleaned_diff) do
+ corrupted_diff.dup.force_encoding('UTF-8')
+ .encode!('UTF-8', invalid: :replace, replace: '')
+ end
+
+ let(:encoded_diff) do
+ described_class.encode!(corrupted_diff.dup)
+ end
+
+ it 'does not corrupt data but remove invalid characters' do
+ expect(encoded_diff).to eq(cleaned_diff)
+ end
+
+ def commit(repo, path, content)
+ oid = repo.write(content, :blob)
+ index = repo.index
+
+ index.read_tree(repo.head.target.tree) unless repo.empty?
+
+ index.add(path: path, oid: oid, mode: 0100644)
+ user = { name: 'Test', email: 'test@example.com' }
+
+ Rugged::Commit.create(
+ repo,
+ tree: index.write_tree(repo),
+ author: user,
+ committer: user,
+ message: "Update #{path}",
+ parents: repo.empty? ? [] : [repo.head.target].compact,
+ update_ref: 'HEAD'
+ )
+ end
+ end
end
describe '#encode_utf8' do
diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb
index c1ed47cf64a..7322a326b01 100644
--- a/spec/lib/gitlab/exclusive_lease_spec.rb
+++ b/spec/lib/gitlab/exclusive_lease_spec.rb
@@ -47,6 +47,18 @@ describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do
end
end
+ describe '.get_uuid' do
+ it 'gets the uuid if lease with the key associated exists' do
+ uuid = described_class.new(unique_key, timeout: 3600).try_obtain
+
+ expect(described_class.get_uuid(unique_key)).to eq(uuid)
+ end
+
+ it 'returns false if the lease does not exist' do
+ expect(described_class.get_uuid(unique_key)).to be false
+ end
+ end
+
describe '.cancel' do
it 'can cancel a lease' do
uuid = new_lease(unique_key)
diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb
index 3fb6315a39a..07cb10e563e 100644
--- a/spec/lib/gitlab/file_finder_spec.rb
+++ b/spec/lib/gitlab/file_finder_spec.rb
@@ -7,15 +7,23 @@ describe Gitlab::FileFinder do
it 'finds by name' do
results = finder.find('files')
- expect(results.map(&:first)).to include('files/images/wm.svg')
+
+ filename, blob = results.find { |_, blob| blob.filename == 'files/images/wm.svg' }
+ expect(filename).to eq('files/images/wm.svg')
+ expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
+ expect(blob.ref).to eq(finder.ref)
+ expect(blob.data).not_to be_empty
end
it 'finds by content' do
results = finder.find('files')
- blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last
+ filename, blob = results.find { |_, blob| blob.filename == 'CHANGELOG' }
- expect(blob.filename).to eq("CHANGELOG")
+ expect(filename).to eq('CHANGELOG')
+ expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
+ expect(blob.ref).to eq(finder.ref)
+ expect(blob.data).not_to be_empty
end
end
end
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 800c245b130..465c2012b05 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
describe Gitlab::Git::Blame, seed_helper: true do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
let(:blame) do
Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md")
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index dfab0c2fe85..66ba00acb7d 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -3,7 +3,7 @@
require "spec_helper"
describe Gitlab::Git::Blob, seed_helper: true do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
describe 'initialize' do
let(:blob) { Gitlab::Git::Blob.new(name: 'test') }
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index cdf1b8beee3..318a7b7a332 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::Git::Branch, seed_helper: true do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
subject { repository.branches }
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 26d7a364f5b..14d64d8c4da 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -1,15 +1,15 @@
require "spec_helper"
describe Gitlab::Git::Commit, seed_helper: true do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
- let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
+ let(:commit) { described_class.find(repository, SeedRepo::Commit::ID) }
let(:rugged_commit) do
repository.rugged.lookup(SeedRepo::Commit::ID)
end
describe "Commit info" do
before do
- repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH).rugged
+ repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
@committer = {
email: 'mike@smith.com',
@@ -24,7 +24,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
}
@parents = [repo.head.target]
- @gitlab_parents = @parents.map { |c| Gitlab::Git::Commit.decorate(c) }
+ @gitlab_parents = @parents.map { |c| described_class.decorate(repository, c) }
@tree = @parents.first.tree
sha = Rugged::Commit.create(
@@ -38,7 +38,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
)
@raw_commit = repo.lookup(sha)
- @commit = Gitlab::Git::Commit.new(@raw_commit)
+ @commit = described_class.new(repository, @raw_commit)
end
it { expect(@commit.short_id).to eq(@raw_commit.oid[0..10]) }
@@ -59,13 +59,14 @@ describe Gitlab::Git::Commit, seed_helper: true do
after do
# Erase the new commit so other tests get the original repo
- repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH).rugged
+ repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
end
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
@@ -88,10 +89,11 @@ describe Gitlab::Git::Commit, seed_helper: true do
subject: subject,
body: body,
author: author,
- committer: committer
+ committer: committer,
+ parent_ids: parent_ids
)
end
- let(:commit) { described_class.new(Gitlab::GitalyClient::Commit.new(repository, gitaly_commit)) }
+ let(:commit) { described_class.new(repository, gitaly_commit) }
it { expect(commit.short_id).to eq(id[0..10]) }
it { expect(commit.id).to eq(id) }
@@ -102,6 +104,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) }
context 'no body' do
let(:body) { "".force_encoding('ASCII-8BIT') }
@@ -113,45 +116,45 @@ describe Gitlab::Git::Commit, seed_helper: true do
context 'Class methods' do
describe '.find' do
it "should return first head commit if without params" do
- expect(Gitlab::Git::Commit.last(repository).id).to eq(
+ expect(described_class.last(repository).id).to eq(
repository.rugged.head.target.oid
)
end
it "should return valid commit" do
- expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_valid_commit
+ expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_valid_commit
end
it "should return valid commit for tag" do
- expect(Gitlab::Git::Commit.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ expect(described_class.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
end
it "should return nil for non-commit ids" do
blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
- expect(Gitlab::Git::Commit.find(repository, blob.id)).to be_nil
+ expect(described_class.find(repository, blob.id)).to be_nil
end
it "should return nil for parent of non-commit object" do
blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
- expect(Gitlab::Git::Commit.find(repository, "#{blob.id}^")).to be_nil
+ expect(described_class.find(repository, "#{blob.id}^")).to be_nil
end
it "should return nil for nonexisting ids" do
- expect(Gitlab::Git::Commit.find(repository, "+123_4532530XYZ")).to be_nil
+ expect(described_class.find(repository, "+123_4532530XYZ")).to be_nil
end
context 'with broken repo' do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH, '') }
it 'returns nil' do
- expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_nil
+ expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_nil
end
end
end
describe '.last_for_path' do
context 'no path' do
- subject { Gitlab::Git::Commit.last_for_path(repository, 'master') }
+ subject { described_class.last_for_path(repository, 'master') }
describe '#id' do
subject { super().id }
@@ -160,7 +163,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
context 'path' do
- subject { Gitlab::Git::Commit.last_for_path(repository, 'master', 'files/ruby') }
+ subject { described_class.last_for_path(repository, 'master', 'files/ruby') }
describe '#id' do
subject { super().id }
@@ -169,7 +172,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
context 'ref + path' do
- subject { Gitlab::Git::Commit.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') }
+ subject { described_class.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') }
describe '#id' do
subject { super().id }
@@ -181,7 +184,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
describe '.where' do
context 'path is empty string' do
subject do
- commits = Gitlab::Git::Commit.where(
+ commits = described_class.where(
repo: repository,
ref: 'master',
path: '',
@@ -199,7 +202,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
context 'path is nil' do
subject do
- commits = Gitlab::Git::Commit.where(
+ commits = described_class.where(
repo: repository,
ref: 'master',
path: nil,
@@ -217,7 +220,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
context 'ref is branch name' do
subject do
- commits = Gitlab::Git::Commit.where(
+ commits = described_class.where(
repo: repository,
ref: 'master',
path: 'files',
@@ -237,7 +240,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
context 'ref is commit id' do
subject do
- commits = Gitlab::Git::Commit.where(
+ commits = described_class.where(
repo: repository,
ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e",
path: 'files',
@@ -257,7 +260,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
context 'ref is tag' do
subject do
- commits = Gitlab::Git::Commit.where(
+ commits = described_class.where(
repo: repository,
ref: 'v1.0.0',
path: 'files',
@@ -278,7 +281,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
describe '.between' do
subject do
- commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID)
+ commits = described_class.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID)
commits.map { |c| c.id }
end
@@ -294,12 +297,12 @@ describe Gitlab::Git::Commit, seed_helper: true do
it 'should return a return a collection of commits' do
commits = described_class.find_all(repository)
- expect(commits).to all( be_a_kind_of(Gitlab::Git::Commit) )
+ expect(commits).to all( be_a_kind_of(described_class) )
end
context 'max_count' do
subject do
- commits = Gitlab::Git::Commit.find_all(
+ commits = described_class.find_all(
repository,
max_count: 50
)
@@ -307,8 +310,8 @@ describe Gitlab::Git::Commit, seed_helper: true do
commits.map(&:id)
end
- it 'has 33 elements' do
- expect(subject.size).to eq(33)
+ it 'has 34 elements' do
+ expect(subject.size).to eq(34)
end
it 'includes the expected commits' do
@@ -322,7 +325,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
context 'ref + max_count + skip' do
subject do
- commits = Gitlab::Git::Commit.find_all(
+ commits = described_class.find_all(
repository,
ref: 'master',
max_count: 50,
@@ -374,7 +377,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
describe '#init_from_rugged' do
- let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) }
+ let(:gitlab_commit) { described_class.new(repository, rugged_commit) }
subject { gitlab_commit }
describe '#id' do
@@ -384,7 +387,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
describe '#init_from_hash' do
- let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) }
+ let(:commit) { described_class.new(repository, sample_commit_hash) }
subject { commit }
describe '#id' do
@@ -451,7 +454,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
describe '#ref_names' do
- let(:commit) { Gitlab::Git::Commit.find(repository, 'master') }
+ let(:commit) { described_class.find(repository, 'master') }
subject { commit.ref_names(repository) }
it 'has 1 element' do
diff --git a/spec/lib/gitlab/git/committer_spec.rb b/spec/lib/gitlab/git/committer_spec.rb
new file mode 100644
index 00000000000..b0ddbb51449
--- /dev/null
+++ b/spec/lib/gitlab/git/committer_spec.rb
@@ -0,0 +1,22 @@
+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/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb
index 4c9f4a28f32..b6a42e422b5 100644
--- a/spec/lib/gitlab/git/compare_spec.rb
+++ b/spec/lib/gitlab/git/compare_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::Git::Compare, seed_helper: true do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: false) }
let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: true) }
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index 0cfb210e390..3494f0cc98d 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -384,7 +384,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
context 'when go over safe limits on files' do
let(:iterator) { [fake_diff(1, 1)] * 4 }
- before(:each) do
+ before do
stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines })
end
@@ -409,7 +409,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
]
end
- before(:each) do
+ before do
stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 })
end
@@ -434,7 +434,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
]
end
- before(:each) do
+ before do
stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 })
end
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 7ea3386ac2a..d39b33a0c05 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::Git::Diff, seed_helper: true do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
before do
@raw_diff_hash = {
@@ -273,6 +273,25 @@ EOT
end
end
+ describe '#json_safe_diff' do
+ let(:project) { create(:project, :repository) }
+
+ it 'fake binary message when it detects binary' do
+ # Rugged will not detect this as binary, but we can fake it
+ diff_message = "Binary files files/images/icn-time-tracking.pdf and files/images/icn-time-tracking.pdf differ\n"
+ binary_diff = described_class.between(project.repository, 'add-pdf-text-binary', 'add-pdf-text-binary^').first
+
+ expect(binary_diff.diff).not_to be_empty
+ expect(binary_diff.json_safe_diff).to eq(diff_message)
+ end
+
+ it 'leave non-binary diffs as-is' do
+ diff = described_class.new(@rugged_diff)
+
+ expect(diff.json_safe_diff).to eq(diff.diff)
+ end
+ end
+
describe '#submodule?' do
before do
commit = repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb
index 19391a70cf6..ea3e4680b1d 100644
--- a/spec/lib/gitlab/git/hook_spec.rb
+++ b/spec/lib/gitlab/git/hook_spec.rb
@@ -10,7 +10,8 @@ describe Gitlab::Git::Hook do
describe "#trigger" do
let(:project) { create(:project, :repository) }
- let(:repo_path) { project.repository.path }
+ let(:repository) { project.repository.raw_repository }
+ let(:repo_path) { repository.path }
let(:user) { create(:user) }
let(:gl_id) { Gitlab::GlId.gl_id(user) }
@@ -48,7 +49,7 @@ describe Gitlab::Git::Hook do
it "returns success with no errors" do
create_hook(hook_name)
- hook = described_class.new(hook_name, project)
+ hook = described_class.new(hook_name, repository)
blank = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
@@ -66,7 +67,7 @@ describe Gitlab::Git::Hook do
context "when the hook is unsuccessful" do
it "returns failure with errors" do
create_failing_hook(hook_name)
- hook = described_class.new(hook_name, project)
+ hook = described_class.new(hook_name, repository)
blank = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
@@ -80,7 +81,7 @@ describe Gitlab::Git::Hook do
context "when the hook doesn't exist" do
it "returns success with no errors" do
- hook = described_class.new('unknown_hook', project)
+ hook = described_class.new('unknown_hook', repository)
blank = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch'
diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb
new file mode 100644
index 00000000000..e9c0209fe3b
--- /dev/null
+++ b/spec/lib/gitlab/git/hooks_service_spec.rb
@@ -0,0 +1,48 @@
+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(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') }
+ let(:service) { described_class.new }
+
+ before do
+ @blankrev = Gitlab::Git::BLANK_SHA
+ @oldrev = SeedRepo::Commit::PARENT_ID
+ @newrev = SeedRepo::Commit::ID
+ @ref = 'refs/heads/feature'
+ end
+
+ describe '#execute' do
+ context 'when receive hooks were successful' do
+ it 'calls post-receive hook' 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) { }
+ end
+ end
+
+ context 'when pre-receive hook failed' do
+ it 'does not call post-receive hook' do
+ expect(service).to receive(:run_hook).with('pre-receive').and_return([false, ''])
+ expect(service).not_to receive(:run_hook).with('post-receive')
+
+ expect do
+ service.execute(committer, repository, @blankrev, @newrev, @ref)
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ end
+ end
+
+ context 'when update hook failed' do
+ it 'does not call post-receive hook' do
+ expect(service).to receive(:run_hook).with('pre-receive').and_return([true, nil])
+ expect(service).to receive(:run_hook).with('update').and_return([false, ''])
+ expect(service).not_to receive(:run_hook).with('post-receive')
+
+ expect do
+ service.execute(committer, repository, @blankrev, @newrev, @ref)
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb
index 21b71654251..73fbc6a6afa 100644
--- a/spec/lib/gitlab/git/index_spec.rb
+++ b/spec/lib/gitlab/git/index_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Git::Index, seed_helper: true do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
let(:index) { described_class.new(repository) }
before do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 20d6b58d6d1..556a148c3bc 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
describe "Respond to" do
subject { repository }
@@ -56,14 +56,14 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe "#rugged" do
describe 'when storage is broken', broken_storage: true do
it 'raises a storage exception when storage is not available' do
- broken_repo = described_class.new('broken', 'a/path.git')
+ broken_repo = described_class.new('broken', 'a/path.git', '')
expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Storage::Inaccessible)
end
end
it 'raises a no repository exception when there is no repo' do
- broken_repo = described_class.new('default', 'a/path.git')
+ broken_repo = described_class.new('default', 'a/path.git', '')
expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Repository::NoRepository)
end
@@ -235,18 +235,10 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.to be < 2 }
end
- describe '#has_commits?' do
- it { expect(repository.has_commits?).to be_truthy }
- end
-
describe '#empty?' do
it { expect(repository.empty?).to be_falsey }
end
- describe '#bare?' do
- it { expect(repository.bare?).to be_truthy }
- end
-
describe '#ref_names' do
let(:ref_names) { repository.ref_names }
subject { ref_names }
@@ -265,7 +257,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#submodule_url_for' do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
let(:ref) { 'master' }
def submodule_url(path)
@@ -289,7 +281,13 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') }
end
- context 'no submodules at commit' do
+ context 'no .gitmodules at commit' do
+ let(:ref) { '9596bc54a6f0c0c98248fe97077eb5ccf48a98d0' }
+
+ it { expect(submodule_url('six')).to eq(nil) }
+ end
+
+ context 'no gitlink entry' do
let(:ref) { '6d39438' }
it { expect(submodule_url('six')).to eq(nil) }
@@ -297,7 +295,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
context '#submodules' do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
context 'where repo has submodules' do
let(:submodules) { repository.send(:submodules, 'master') }
@@ -392,55 +390,107 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe "#delete_branch" do
- before(:all) do
- @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH)
- @repo.delete_branch("feature")
+ shared_examples "deleting a branch" do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
+
+ after do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+
+ it "removes the branch from the repo" do
+ branch_name = "to-be-deleted-soon"
+
+ repository.create_branch(branch_name)
+ expect(repository.rugged.branches[branch_name]).not_to be_nil
+
+ repository.delete_branch(branch_name)
+ expect(repository.rugged.branches[branch_name]).to be_nil
+ end
+
+ context "when branch does not exist" do
+ it "raises a DeleteBranchError exception" do
+ expect { repository.delete_branch("this-branch-does-not-exist") }.to raise_error(Gitlab::Git::Repository::DeleteBranchError)
+ end
+ end
end
- it "should remove the branch from the repo" do
- expect(@repo.rugged.branches["feature"]).to be_nil
+ context "when Gitaly delete_branch is enabled" do
+ it_behaves_like "deleting a branch"
end
- after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
- ensure_seeds
+ context "when Gitaly delete_branch is disabled", skip_gitaly_mock: true do
+ it_behaves_like "deleting a branch"
end
end
describe "#create_branch" do
- before(:all) do
- @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH)
+ shared_examples 'creating a branch' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
+
+ after do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+
+ it "should create a new branch" do
+ expect(repository.create_branch('new_branch', 'master')).not_to be_nil
+ end
+
+ it "should create a new branch with the right name" do
+ expect(repository.create_branch('another_branch', 'master').name).to eq('another_branch')
+ end
+
+ it "should fail if we create an existing branch" do
+ repository.create_branch('duplicated_branch', 'master')
+ expect {repository.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists")
+ end
+
+ it "should fail if we create a branch from a non existing ref" do
+ expect {repository.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge")
+ end
end
- it "should create a new branch" do
- expect(@repo.create_branch('new_branch', 'master')).not_to be_nil
+ context 'when Gitaly create_branch feature is enabled' do
+ it_behaves_like 'creating a branch'
end
- it "should create a new branch with the right name" do
- expect(@repo.create_branch('another_branch', 'master').name).to eq('another_branch')
+ context 'when Gitaly create_branch feature is disabled', skip_gitaly_mock: true do
+ it_behaves_like 'creating a branch'
end
+ end
- it "should fail if we create an existing branch" do
- @repo.create_branch('duplicated_branch', 'master')
- expect{@repo.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists")
+ describe '#delete_refs' do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
- it "should fail if we create a branch from a non existing ref" do
- expect{@repo.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge")
+ it 'deletes the ref' do
+ @repo.delete_refs('refs/heads/feature')
+
+ expect(@repo.rugged.references['refs/heads/feature']).to be_nil
end
- after(:all) do
- FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
- ensure_seeds
+ it 'deletes all refs' do
+ refs = %w[refs/heads/wip refs/tags/v1.1.0]
+ @repo.delete_refs(*refs)
+
+ refs.each do |ref|
+ expect(@repo.rugged.references[ref]).to be_nil
+ end
end
- end
- describe "#remote_names" do
- let(:remotes) { repository.remote_names }
+ it 'raises an error if it failed' do
+ expect(Gitlab::Popen).to receive(:popen).and_return(['Error', 1])
+
+ expect do
+ @repo.delete_refs('refs/heads/fix')
+ end.to raise_error(Gitlab::Git::Repository::GitError)
+ end
- it "should have one entry: 'origin'" do
- expect(remotes.size).to eq(1)
- expect(remotes.first).to eq("origin")
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
end
end
@@ -456,7 +506,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe "#remote_delete" do
before(:all) do
- @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH)
+ @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
@repo.remote_delete("expendable")
end
@@ -472,7 +522,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe "#remote_add" do
before(:all) do
- @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH)
+ @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
@repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL)
end
@@ -488,7 +538,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe "#remote_update" do
before(:all) do
- @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH)
+ @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
@repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH)
end
@@ -505,22 +555,27 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe "#log" do
- commit_with_old_name = nil
- commit_with_new_name = nil
- rename_commit = nil
+ let(:commit_with_old_name) do
+ Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id)
+ end
+ let(:commit_with_new_name) do
+ Gitlab::Git::Commit.decorate(repository, @commit_with_new_name_id)
+ end
+ let(:rename_commit) do
+ Gitlab::Git::Commit.decorate(repository, @rename_commit_id)
+ end
before(:context) do
# Add new commits so that there's a renamed file in the commit history
- repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH).rugged
-
- commit_with_old_name = Gitlab::Git::Commit.decorate(new_commit_edit_old_file(repo))
- rename_commit = Gitlab::Git::Commit.decorate(new_commit_move_file(repo))
- commit_with_new_name = Gitlab::Git::Commit.decorate(new_commit_edit_new_file(repo))
+ repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
+ @commit_with_old_name_id = new_commit_edit_old_file(repo)
+ @rename_commit_id = new_commit_move_file(repo)
+ @commit_with_new_name_id = new_commit_edit_new_file(repo)
end
after(:context) do
# Erase our commits so other tests get the original repo
- repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH).rugged
+ repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
end
@@ -754,7 +809,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } }
def commit_files(commit)
- commit.diff_from_parent.deltas.flat_map do |delta|
+ commit.rugged_diff_from_parent.deltas.flat_map do |delta|
[delta.old_file[:path], delta.new_file[:path]].uniq.compact
end
end
@@ -855,7 +910,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#autocrlf' do
before(:all) do
- @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH)
+ @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
@repo.rugged.config['core.autocrlf'] = true
end
@@ -870,14 +925,14 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#autocrlf=' do
before(:all) do
- @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH)
+ @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
@repo.rugged.config['core.autocrlf'] = false
end
it 'should set the autocrlf option to the provided option' do
@repo.autocrlf = :input
- File.open(File.join(SEED_STORAGE_PATH, TEST_MUTABLE_REPO_PATH, '.git', 'config')) do |config_file|
+ File.open(File.join(SEED_STORAGE_PATH, TEST_MUTABLE_REPO_PATH, 'config')) do |config_file|
expect(config_file.read).to match('autocrlf = input')
end
end
@@ -888,27 +943,37 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#find_branch' do
- it 'should return a Branch for master' do
- branch = repository.find_branch('master')
+ shared_examples 'finding a branch' do
+ it 'should return a Branch for master' do
+ branch = repository.find_branch('master')
- expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
- expect(branch.name).to eq('master')
- end
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
- it 'should handle non-existent branch' do
- branch = repository.find_branch('this-is-garbage')
+ it 'should handle non-existent branch' do
+ branch = repository.find_branch('this-is-garbage')
- expect(branch).to eq(nil)
+ expect(branch).to eq(nil)
+ end
end
- it 'should reload Rugged::Repository and return master' do
- expect(Rugged::Repository).to receive(:new).twice.and_call_original
+ context 'when Gitaly find_branch feature is enabled' do
+ it_behaves_like 'finding a branch'
+ end
+
+ context 'when Gitaly find_branch feature is disabled', skip_gitaly_mock: true do
+ it_behaves_like 'finding a branch'
- repository.find_branch('master')
- branch = repository.find_branch('master', force_reload: true)
+ it 'should reload Rugged::Repository and return master' do
+ expect(Rugged::Repository).to receive(:new).twice.and_call_original
- expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
- expect(branch.name).to eq('master')
+ repository.find_branch('master')
+ branch = repository.find_branch('master', force_reload: true)
+
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
end
end
@@ -939,7 +1004,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'with local and remote branches' do
let(:repository) do
- Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git'))
+ Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
before do
@@ -981,7 +1046,37 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#branch_count' do
it 'returns the number of branches' do
- expect(repository.branch_count).to eq(9)
+ expect(repository.branch_count).to eq(10)
+ end
+
+ context 'with local and remote branches' do
+ let(:repository) do
+ Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
+ end
+
+ before do
+ create_remote_branch(repository, 'joe', 'remote_branch', 'master')
+ repository.create_branch('local_branch', 'master')
+ end
+
+ after do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+
+ it 'returns the count of local branches' do
+ expect(repository.branch_count).to eq(repository.local_branches.count)
+ end
+
+ context 'with Gitaly disabled' do
+ before do
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false)
+ end
+
+ it 'returns the count of local branches' do
+ expect(repository.branch_count).to eq(repository.local_branches.count)
+ end
+ end
end
end
@@ -997,7 +1092,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(master_file_paths).to include("files/html/500.html")
end
- it "dose not read submodule directory and empty directory of master branch" do
+ it "does not read submodule directory and empty directory of master branch" do
expect(master_file_paths).not_to include("six")
end
@@ -1018,7 +1113,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
context "with no .gitattrbutes" do
- before(:each) do
+ before do
repository.copy_gitattributes("master")
end
@@ -1026,13 +1121,13 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(File.exist?(attributes_path)).to be_falsey
end
- after(:each) do
+ after do
FileUtils.rm_rf(attributes_path)
end
end
context "with .gitattrbutes" do
- before(:each) do
+ before do
repository.copy_gitattributes("gitattributes")
end
@@ -1045,13 +1140,13 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(contents).to eq("*.md binary\n")
end
- after(:each) do
+ after do
FileUtils.rm_rf(attributes_path)
end
end
context "with updated .gitattrbutes" do
- before(:each) do
+ before do
repository.copy_gitattributes("gitattributes")
repository.copy_gitattributes("gitattributes-updated")
end
@@ -1065,13 +1160,13 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(contents).to eq("*.txt binary\n")
end
- after(:each) do
+ after do
FileUtils.rm_rf(attributes_path)
end
end
context "with no .gitattrbutes in HEAD but with previous info/attributes" do
- before(:each) do
+ before do
repository.copy_gitattributes("gitattributes")
repository.copy_gitattributes("master")
end
@@ -1080,41 +1175,89 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(File.exist?(attributes_path)).to be_falsey
end
- after(:each) do
+ after do
FileUtils.rm_rf(attributes_path)
end
end
end
+ describe '#ref_exists?' do
+ shared_examples 'checks the existence of refs' do
+ it 'returns true for an existing tag' do
+ expect(repository.ref_exists?('refs/heads/master')).to eq(true)
+ end
+
+ it 'returns false for a non-existing tag' do
+ expect(repository.ref_exists?('refs/tags/THIS_TAG_DOES_NOT_EXIST')).to eq(false)
+ end
+
+ it 'raises an ArgumentError for an empty string' do
+ expect { repository.ref_exists?('') }.to raise_error(ArgumentError)
+ end
+
+ it 'raises an ArgumentError for an invalid ref' do
+ expect { repository.ref_exists?('INVALID') }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when Gitaly ref_exists feature is enabled' do
+ it_behaves_like 'checks the existence of refs'
+ end
+
+ context 'when Gitaly ref_exists feature is disabled', skip_gitaly_mock: true do
+ it_behaves_like 'checks the existence of refs'
+ end
+ end
+
describe '#tag_exists?' do
- it 'returns true for an existing tag' do
- tag = repository.tag_names.first
+ shared_examples 'checks the existence of tags' do
+ it 'returns true for an existing tag' do
+ tag = repository.tag_names.first
+
+ expect(repository.tag_exists?(tag)).to eq(true)
+ end
+
+ it 'returns false for a non-existing tag' do
+ expect(repository.tag_exists?('v9000')).to eq(false)
+ end
+ end
- expect(repository.tag_exists?(tag)).to eq(true)
+ context 'when Gitaly ref_exists_tags feature is enabled' do
+ it_behaves_like 'checks the existence of tags'
end
- it 'returns false for a non-existing tag' do
- expect(repository.tag_exists?('v9000')).to eq(false)
+ context 'when Gitaly ref_exists_tags feature is disabled', skip_gitaly_mock: true do
+ it_behaves_like 'checks the existence of tags'
end
end
describe '#branch_exists?' do
- it 'returns true for an existing branch' do
- expect(repository.branch_exists?('master')).to eq(true)
+ shared_examples 'checks the existence of branches' do
+ it 'returns true for an existing branch' do
+ expect(repository.branch_exists?('master')).to eq(true)
+ end
+
+ it 'returns false for a non-existing branch' do
+ expect(repository.branch_exists?('kittens')).to eq(false)
+ end
+
+ it 'returns false when using an invalid branch name' do
+ expect(repository.branch_exists?('.bla')).to eq(false)
+ end
end
- it 'returns false for a non-existing branch' do
- expect(repository.branch_exists?('kittens')).to eq(false)
+ context 'when Gitaly ref_exists_branches feature is enabled' do
+ it_behaves_like 'checks the existence of branches'
end
- it 'returns false when using an invalid branch name' do
- expect(repository.branch_exists?('.bla')).to eq(false)
+ context 'when Gitaly ref_exists_branches feature is disabled', skip_gitaly_mock: true do
+ it_behaves_like 'checks the existence of branches'
end
end
describe '#local_branches' do
before(:all) do
- @repo = Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git'))
+ @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
after(:all) do
diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
index b2886628601..c86353abb7c 100644
--- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
+++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
@@ -1,9 +1,30 @@
require 'spec_helper'
describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do
- let(:circuit_breaker) { described_class.new('default') }
+ let(:storage_name) { 'default' }
+ let(:circuit_breaker) { described_class.new(storage_name) }
let(:hostname) { Gitlab::Environment.hostname }
- let(:cache_key) { "storage_accessible:default:#{hostname}" }
+ let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" }
+
+ before do
+ # Override test-settings for the circuitbreaker with something more realistic
+ # for these specs.
+ stub_storage_settings('default' => {
+ 'path' => TestEnv.repos_path,
+ 'failure_count_threshold' => 10,
+ 'failure_wait_time' => 30,
+ 'failure_reset_time' => 1800,
+ 'storage_timeout' => 5
+ },
+ 'broken' => {
+ 'path' => 'tmp/tests/non-existent-repositories',
+ 'failure_count_threshold' => 10,
+ 'failure_wait_time' => 30,
+ 'failure_reset_time' => 1800,
+ 'storage_timeout' => 5
+ }
+ )
+ end
def value_from_redis(name)
Gitlab::Git::Storage.redis.with do |redis|
@@ -96,14 +117,14 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
end
describe '#circuit_broken?' do
- it 'is closed when there is no last failure' do
+ it 'is working when there is no last failure' do
set_in_redis(:last_failure, nil)
set_in_redis(:failure_count, 0)
expect(circuit_breaker.circuit_broken?).to be_falsey
end
- it 'is open when there was a recent failure' do
+ it 'is broken when there was a recent failure' do
Timecop.freeze do
set_in_redis(:last_failure, 1.second.ago.to_f)
set_in_redis(:failure_count, 1)
@@ -112,16 +133,34 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
end
end
- it 'is open when there are to many failures' do
+ it 'is broken when there are too many failures' do
set_in_redis(:last_failure, 1.day.ago.to_f)
set_in_redis(:failure_count, 200)
expect(circuit_breaker.circuit_broken?).to be_truthy
end
+
+ context 'the `failure_wait_time` is set to 0' do
+ before do
+ stub_storage_settings('default' => {
+ 'failure_wait_time' => 0,
+ 'path' => TestEnv.repos_path
+ })
+ end
+
+ it 'is working even when there is a recent failure' do
+ Timecop.freeze do
+ set_in_redis(:last_failure, 0.seconds.ago.to_f)
+ set_in_redis(:failure_count, 1)
+
+ expect(circuit_breaker.circuit_broken?).to be_falsey
+ end
+ end
+ end
end
describe "storage_available?" do
- context 'when the storage is available' do
+ context 'the storage is available' do
it 'tracks that the storage was accessible an raises the error' do
expect(circuit_breaker).to receive(:track_storage_accessible)
@@ -136,8 +175,8 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
end
end
- context 'when storage is not available' do
- let(:circuit_breaker) { described_class.new('broken') }
+ context 'storage is not available' do
+ let(:storage_name) { 'broken' }
it 'tracks that the storage was inaccessible' do
expect(circuit_breaker).to receive(:track_storage_inaccessible)
@@ -158,8 +197,8 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
end
end
- context 'when the storage is not available' do
- let(:circuit_breaker) { described_class.new('broken') }
+ context 'the storage is not available' do
+ let(:storage_name) { 'broken' }
it 'raises an error' do
expect(circuit_breaker).to receive(:track_storage_inaccessible)
@@ -174,12 +213,8 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
end
describe '#track_storage_inaccessible' do
- around(:each) do |example|
- Timecop.freeze
-
- example.run
-
- Timecop.return
+ around do |example|
+ Timecop.freeze { example.run }
end
it 'records the failure time in redis' do
diff --git a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb
index 12366151f44..c708b15853a 100644
--- a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb
+++ b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Git::Storage::ForkedStorageCheck, skip_database_cleaner: true do
+describe Gitlab::Git::Storage::ForkedStorageCheck, broken_storage: true, skip_database_cleaner: true do
let(:existing_path) do
existing_path = TestEnv.repos_path
FileUtils.mkdir_p(existing_path)
diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb
index 78d1e120013..cc10679ef1e 100644
--- a/spec/lib/gitlab/git/tag_spec.rb
+++ b/spec/lib/gitlab/git/tag_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::Git::Tag, seed_helper: true do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
shared_examples 'Gitlab::Git::Repository#tags' do
describe 'first tag' do
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index 98ddd3c3664..86f7bcb8e38 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::Git::Tree, seed_helper: true do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
context :repo do
let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) }
@@ -20,6 +20,7 @@ describe Gitlab::Git::Tree, seed_helper: true do
it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(dir.name).to eq('encoding') }
it { expect(dir.path).to eq('encoding') }
+ it { expect(dir.flat_path).to eq('encoding') }
it { expect(dir.mode).to eq('40000') }
context :subdir do
@@ -30,6 +31,7 @@ describe Gitlab::Git::Tree, seed_helper: true do
it { expect(subdir.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(subdir.name).to eq('html') }
it { expect(subdir.path).to eq('files/html') }
+ it { expect(subdir.flat_path).to eq('files/html') }
end
context :subdir_file do
@@ -40,6 +42,7 @@ describe Gitlab::Git::Tree, seed_helper: true do
it { expect(subdir_file.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(subdir_file.name).to eq('popen.rb') }
it { expect(subdir_file.path).to eq('files/ruby/popen.rb') }
+ it { expect(subdir_file.flat_path).to eq('files/ruby/popen.rb') }
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 2d6ea37d0ac..458627ee4de 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1,21 +1,17 @@
require 'spec_helper'
describe Gitlab::GitAccess do
- let(:pull_access_check) { access.check('git-upload-pack', '_any') }
- let(:push_access_check) { access.check('git-receive-pack', '_any') }
- let(:access) { described_class.new(actor, project, protocol, authentication_abilities: authentication_abilities, redirected_path: redirected_path) }
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
+ set(:user) { create(:user) }
+
let(:actor) { user }
+ let(:project) { create(:project, :repository) }
let(:protocol) { 'ssh' }
+ let(:authentication_abilities) { %i[read_project download_code push_code] }
let(:redirected_path) { nil }
- let(:authentication_abilities) do
- [
- :read_project,
- :download_code,
- :push_code
- ]
- end
+
+ let(:access) { described_class.new(actor, project, protocol, authentication_abilities: authentication_abilities, redirected_path: redirected_path) }
+ let(:push_access_check) { access.check('git-receive-pack', '_any') }
+ let(:pull_access_check) { access.check('git-upload-pack', '_any') }
describe '#check with single protocols allowed' do
def disable_protocol(protocol)
@@ -27,12 +23,11 @@ describe Gitlab::GitAccess do
disable_protocol('ssh')
end
- it 'blocks ssh git push' do
- expect { push_access_check }.to raise_unauthorized('Git access over SSH is not allowed')
- end
-
- it 'blocks ssh git pull' do
- expect { pull_access_check }.to raise_unauthorized('Git access over SSH is not allowed')
+ it 'blocks ssh git push and pull' do
+ aggregate_failures do
+ expect { push_access_check }.to raise_unauthorized('Git access over SSH is not allowed')
+ expect { pull_access_check }.to raise_unauthorized('Git access over SSH is not allowed')
+ end
end
end
@@ -43,12 +38,11 @@ describe Gitlab::GitAccess do
disable_protocol('http')
end
- it 'blocks http push' do
- expect { push_access_check }.to raise_unauthorized('Git access over HTTP is not allowed')
- end
-
- it 'blocks http git pull' do
- expect { pull_access_check }.to raise_unauthorized('Git access over HTTP is not allowed')
+ it 'blocks http push and pull' do
+ aggregate_failures do
+ expect { push_access_check }.to raise_unauthorized('Git access over HTTP is not allowed')
+ expect { pull_access_check }.to raise_unauthorized('Git access over HTTP is not allowed')
+ end
end
end
end
@@ -65,22 +59,20 @@ describe Gitlab::GitAccess do
deploy_key.projects << project
end
- it 'allows pull access' do
- expect { pull_access_check }.not_to raise_error
- end
-
- it 'allows push access' do
- expect { push_access_check }.not_to raise_error
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ expect { pull_access_check }.not_to raise_error
+ end
end
end
context 'when the Deploykey does not have access to the project' do
- it 'blocks pulls with "not found"' do
- expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.')
- end
-
- it 'blocks pushes with "not found"' do
- expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.')
+ it 'blocks push and pull with "not found"' do
+ aggregate_failures do
+ expect { push_access_check }.to raise_not_found
+ expect { pull_access_check }.to raise_not_found
+ end
end
end
end
@@ -88,25 +80,23 @@ describe Gitlab::GitAccess do
context 'when actor is a User' do
context 'when the User can read the project' do
before do
- project.team << [user, :master]
- end
-
- it 'allows pull access' do
- expect { pull_access_check }.not_to raise_error
+ project.add_master(user)
end
- it 'allows push access' do
- expect { push_access_check }.not_to raise_error
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { pull_access_check }.not_to raise_error
+ expect { push_access_check }.not_to raise_error
+ end
end
end
context 'when the User cannot read the project' do
- it 'blocks pulls with "not found"' do
- expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.')
- end
-
- it 'blocks pushes with "not found"' do
- expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.')
+ it 'blocks push and pull with "not found"' do
+ aggregate_failures do
+ expect { push_access_check }.to raise_not_found
+ expect { pull_access_check }.to raise_not_found
+ end
end
end
end
@@ -121,7 +111,7 @@ describe Gitlab::GitAccess do
end
it 'does not block pushes with "not found"' do
- expect { push_access_check }.to raise_unauthorized('You are not allowed to upload code for this project.')
+ expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload])
end
end
end
@@ -137,17 +127,17 @@ describe Gitlab::GitAccess do
end
it 'does not block pushes with "not found"' do
- expect { push_access_check }.to raise_unauthorized('You are not allowed to upload code for this project.')
+ expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload])
end
end
context 'when guests cannot read the project' do
it 'blocks pulls with "not found"' do
- expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.')
+ expect { pull_access_check }.to raise_not_found
end
it 'blocks pushes with "not found"' do
- expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.')
+ expect { push_access_check }.to raise_not_found
end
end
end
@@ -156,48 +146,88 @@ describe Gitlab::GitAccess do
context 'when the project is nil' do
let(:project) { nil }
- it 'blocks any command with "not found"' do
- expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.')
- expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.')
+ it 'blocks push and pull with "not found"' do
+ aggregate_failures do
+ expect { pull_access_check }.to raise_not_found
+ expect { push_access_check }.to raise_not_found
+ end
end
end
end
- describe '#check_project_moved!' do
+ shared_examples '#check with a key that is not valid' do
before do
- project.team << [user, :master]
+ project.add_master(user)
end
- context 'when a redirect was not followed to find the project' do
- context 'pull code' do
- it { expect { pull_access_check }.not_to raise_error }
+ context 'key is too small' do
+ before do
+ stub_application_setting(rsa_key_restriction: 4096)
end
- context 'push code' do
- it { expect { push_access_check }.not_to raise_error }
+ it 'does not allow keys which are too small', aggregate_failures: true 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.')
+ end
+ end
+
+ context 'key type is not allowed' do
+ before 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
+ 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/)
+ end
+ end
+ end
+
+ it_behaves_like '#check with a key that is not valid' do
+ let(:actor) { build(:rsa_key_2048, user: user) }
+ end
+
+ it_behaves_like '#check with a key that is not valid' do
+ let(:actor) { build(:rsa_deploy_key_2048, user: user) }
+ end
+
+ describe '#check_project_moved!' do
+ before do
+ project.add_master(user)
+ end
+
+ context 'when a redirect was not followed to find the project' do
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ expect { pull_access_check }.not_to raise_error
+ end
end
end
context 'when a redirect was followed to find the project' do
let(:redirected_path) { 'some/other-path' }
- context 'pull code' do
- it { expect { pull_access_check }.to raise_not_found(/Project '#{redirected_path}' was moved to '#{project.full_path}'/) }
- it { expect { pull_access_check }.to raise_not_found(/git remote set-url origin #{project.ssh_url_to_repo}/) }
+ it 'blocks push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /Project '#{redirected_path}' was moved to '#{project.full_path}'/)
+ expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.ssh_url_to_repo}/)
- context 'http protocol' do
- let(:protocol) { 'http' }
- it { expect { pull_access_check }.to raise_not_found(/git remote set-url origin #{project.http_url_to_repo}/) }
+ expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /Project '#{redirected_path}' was moved to '#{project.full_path}'/)
+ expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.ssh_url_to_repo}/)
end
end
- context 'push code' do
- it { expect { push_access_check }.to raise_not_found(/Project '#{redirected_path}' was moved to '#{project.full_path}'/) }
- it { expect { push_access_check }.to raise_not_found(/git remote set-url origin #{project.ssh_url_to_repo}/) }
+ context 'http protocol' do
+ let(:protocol) { 'http' }
- context 'http protocol' do
- let(:protocol) { 'http' }
- it { expect { push_access_check }.to raise_not_found(/git remote set-url origin #{project.http_url_to_repo}/) }
+ it 'includes the path to the project using HTTP' do
+ aggregate_failures do
+ expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
+ expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
+ end
end
end
end
@@ -242,40 +272,28 @@ describe Gitlab::GitAccess do
end
describe '#check_download_access!' do
- describe 'master permissions' do
- before do
- project.team << [user, :master]
- end
+ it 'allows masters to pull' do
+ project.add_master(user)
- context 'pull code' do
- it { expect { pull_access_check }.not_to raise_error }
- end
+ expect { pull_access_check }.not_to raise_error
end
- describe 'guest permissions' do
- before do
- project.team << [user, :guest]
- end
+ it 'disallows guests to pull' do
+ project.add_guest(user)
- context 'pull code' do
- it { expect { pull_access_check }.to raise_unauthorized('You are not allowed to download code from this project.') }
- end
+ expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:download])
end
- describe 'blocked user' do
- before do
- project.team << [user, :master]
- user.block
- end
+ it 'disallows blocked users to pull' do
+ project.add_master(user)
+ user.block
- context 'pull code' do
- it { expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.') }
- end
+ expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.')
end
describe 'without access to project' do
context 'pull code' do
- it { expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.') }
+ it { expect { pull_access_check }.to raise_not_found }
end
context 'when project is public' do
@@ -292,7 +310,7 @@ describe Gitlab::GitAccess do
it 'does not give access to download code' do
public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
- expect { pull_access_check }.to raise_unauthorized('You are not allowed to download code from this project.')
+ expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:download])
end
end
end
@@ -321,13 +339,13 @@ describe Gitlab::GitAccess do
context 'from internal project' do
let(:project) { create(:project, :internal, :repository) }
- it { expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.') }
+ it { expect { pull_access_check }.to raise_not_found }
end
context 'from private project' do
let(:project) { create(:project, :private, :repository) }
- it { expect { pull_access_check }.to raise_not_found('The project you were looking for could not be found.') }
+ it { expect { pull_access_check }.to raise_not_found }
end
end
end
@@ -369,7 +387,7 @@ describe Gitlab::GitAccess do
context 'when is not member of the project' do
context 'pull code' do
- it { expect { pull_access_check }.to raise_unauthorized('You are not allowed to download code from this project.') }
+ it { expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:download]) }
end
end
end
@@ -404,7 +422,7 @@ describe Gitlab::GitAccess do
def stub_git_hooks
# Running the `pre-receive` hook is expensive, and not necessary for this test.
- allow_any_instance_of(GitHooksService).to receive(:execute) do |service, &block|
+ allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) do |service, &block|
block.call(service)
end
end
@@ -428,28 +446,30 @@ describe Gitlab::GitAccess do
end
end
- # Run permission checks for a user
def self.run_permission_checks(permissions_matrix)
- permissions_matrix.keys.each do |role|
- describe "#{role} access" do
- before do
- if role == :admin
- user.update_attribute(:admin, true)
- else
- project.team << [user, role]
- end
+ permissions_matrix.each_pair do |role, matrix|
+ # Run through the entire matrix for this role in one test to avoid
+ # repeated setup.
+ #
+ # Expectations are given a custom failure message proc so that it's
+ # easier to identify which check(s) failed.
+ it "has the correct permissions for #{role}s" do
+ if role == :admin
+ user.update_attribute(:admin, true)
+ else
+ project.team << [user, role]
end
- permissions_matrix[role].each do |action, allowed|
- context action.to_s do
- subject { access.send(:check_push_access!, changes[action]) }
+ aggregate_failures do
+ matrix.each do |action, allowed|
+ check = -> { access.send(:check_push_access!, changes[action]) }
- it do
- if allowed
- expect { subject }.not_to raise_error
- else
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError)
- end
+ if allowed
+ expect(&check).not_to raise_error,
+ -> { "expected #{action} to be allowed" }
+ else
+ expect(&check).to raise_error(Gitlab::GitAccess::UnauthorizedError),
+ -> { "expected #{action} to be disallowed" }
end
end
end
@@ -588,26 +608,26 @@ describe Gitlab::GitAccess do
project.team << [user, :reporter]
end
- it { expect { push_access_check }.to raise_unauthorized('You are not allowed to upload code for this project.') }
+ it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) }
end
context 'when unauthorized' do
context 'to public project' do
let(:project) { create(:project, :public, :repository) }
- it { expect { push_access_check }.to raise_unauthorized('You are not allowed to upload code for this project.') }
+ it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) }
end
context 'to internal project' do
let(:project) { create(:project, :internal, :repository) }
- it { expect { push_access_check }.to raise_unauthorized('You are not allowed to upload code for this project.') }
+ it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) }
end
context 'to private project' do
let(:project) { create(:project, :private, :repository) }
- it { expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.') }
+ it { expect { push_access_check }.to raise_not_found }
end
end
end
@@ -631,19 +651,19 @@ describe Gitlab::GitAccess do
context 'to public project' do
let(:project) { create(:project, :public, :repository) }
- it { expect { push_access_check }.to raise_unauthorized('This deploy key does not have write access to this project.') }
+ it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
end
context 'to internal project' do
let(:project) { create(:project, :internal, :repository) }
- it { expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.') }
+ it { expect { push_access_check }.to raise_not_found }
end
context 'to private project' do
let(:project) { create(:project, :private, :repository) }
- it { expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.') }
+ it { expect { push_access_check }.to raise_not_found }
end
end
end
@@ -656,26 +676,26 @@ describe Gitlab::GitAccess do
key.projects << project
end
- it { expect { push_access_check }.to raise_unauthorized('This deploy key does not have write access to this project.') }
+ it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
end
context 'when unauthorized' do
context 'to public project' do
let(:project) { create(:project, :public, :repository) }
- it { expect { push_access_check }.to raise_unauthorized('This deploy key does not have write access to this project.') }
+ it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:deploy_key_upload]) }
end
context 'to internal project' do
let(:project) { create(:project, :internal, :repository) }
- it { expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.') }
+ it { expect { push_access_check }.to raise_not_found }
end
context 'to private project' do
let(:project) { create(:project, :private, :repository) }
- it { expect { push_access_check }.to raise_not_found('The project you were looking for could not be found.') }
+ it { expect { push_access_check }.to raise_not_found }
end
end
end
@@ -687,8 +707,9 @@ describe Gitlab::GitAccess do
raise_error(Gitlab::GitAccess::UnauthorizedError, message)
end
- def raise_not_found(message)
- raise_error(Gitlab::GitAccess::NotFoundError, message)
+ def raise_not_found
+ raise_error(Gitlab::GitAccess::NotFoundError,
+ Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found])
end
def build_authentication_abilities
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index d71e0f84c65..f32fe5d8150 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::GitalyClient::CommitService do
context 'when a commit does not have a parent' do
it 'sends an RPC request with empty tree ref as left commit' do
- initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
+ initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863').raw
request = Gitaly::CommitDiffRequest.new(
repository: repository_message,
left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
@@ -46,18 +46,10 @@ describe Gitlab::GitalyClient::CommitService do
end
end
- it 'returns a Gitlab::Git::DiffCollection' do
+ it 'returns a Gitlab::GitalyClient::DiffStitcher' do
ret = client.diff_from_parent(commit)
- expect(ret).to be_kind_of(Gitlab::Git::DiffCollection)
- end
-
- it 'passes options to Gitlab::Git::DiffCollection' do
- options = { max_files: 31, max_lines: 13, from_gitaly: true }
-
- expect(Gitlab::Git::DiffCollection).to receive(:new).with(kind_of(Enumerable), options)
-
- client.diff_from_parent(commit, options)
+ expect(ret).to be_kind_of(Gitlab::GitalyClient::DiffStitcher)
end
end
@@ -119,5 +111,58 @@ describe Gitlab::GitalyClient::CommitService do
client.tree_entries(repository, revision, path)
end
+
+ context 'with UTF-8 params strings' do
+ let(:revision) { "branch\u011F" }
+ let(:path) { "foo/\u011F.txt" }
+
+ it 'handles string encodings correctly' do
+ expect_any_instance_of(Gitaly::CommitService::Stub)
+ .to receive(:get_tree_entries)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return([])
+
+ client.tree_entries(repository, revision, path)
+ end
+ end
+ end
+
+ describe '#find_commit' do
+ let(:revision) { '4b825dc642cb6eb9a060e54bf8d69288fbee4904' }
+ it 'sends an RPC request' do
+ request = Gitaly::FindCommitRequest.new(
+ repository: repository_message, revision: revision
+ )
+
+ expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit)
+ .with(request, kind_of(Hash)).and_return(double(commit: nil))
+
+ described_class.new(repository).find_commit(revision)
+ end
+ end
+
+ describe '#patch' do
+ let(:request) do
+ Gitaly::CommitPatchRequest.new(
+ repository: repository_message, revision: revision
+ )
+ end
+ let(:response) { [double(data: "my "), double(data: "diff")] }
+
+ subject { described_class.new(repository).patch(revision) }
+
+ it 'sends an RPC request' do
+ expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_patch)
+ .with(request, kind_of(Hash)).and_return([])
+
+ subject
+ end
+
+ it 'concatenates the responses data' do
+ allow_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_patch)
+ .with(request, kind_of(Hash)).and_return(response)
+
+ expect(subject).to eq("my diff")
+ 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 46efc1b18f0..6f59750b4da 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -1,10 +1,11 @@
require 'spec_helper'
describe Gitlab::GitalyClient::RefService do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:storage_name) { project.repository_storage }
let(:relative_path) { project.disk_path + '.git' }
- let(:client) { described_class.new(project.repository) }
+ let(:repository) { project.repository }
+ let(:client) { described_class.new(repository) }
describe '#branches' do
it 'sends a find_all_branches message' do
@@ -84,11 +85,23 @@ describe Gitlab::GitalyClient::RefService do
end
describe '#find_ref_name', seed_helper: true do
- let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
- let(:client) { described_class.new(repository) }
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
+ it 'finds the master branch ref' do
+ expect(client.ref_exists?('refs/heads/master')).to eq(true)
+ end
+
+ it 'returns false for an illegal tag name ref' do
+ expect(client.ref_exists?('refs/tags/.this-tag-name-is-illegal')).to eq(false)
+ end
+
+ it 'raises an argument error if the ref name parameter does not start with refs/' do
+ expect { client.ref_exists?('reXXXXX') }.to raise_error(ArgumentError)
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 5c9c4ed1d7c..fd5f984601e 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -1,19 +1,76 @@
require 'spec_helper'
describe Gitlab::GitalyClient::RepositoryService do
- set(:project) { create(:project) }
+ let(:project) { create(:project) }
let(:storage_name) { project.repository_storage }
- let(:relative_path) { project.path_with_namespace + '.git' }
+ let(:relative_path) { project.disk_path + '.git' }
let(:client) { described_class.new(project.repository) }
describe '#exists?' do
- it 'sends an exists message' do
+ it 'sends a repository_exists message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
- .to receive(:exists)
+ .to receive(:repository_exists)
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
- .and_call_original
+ .and_return(double(exists: true))
client.exists?
end
end
+
+ describe '#garbage_collect' do
+ it 'sends a garbage_collect message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:garbage_collect)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double(:garbage_collect_response))
+
+ client.garbage_collect(true)
+ end
+ end
+
+ describe '#repack_full' do
+ it 'sends a repack_full message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:repack_full)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double(:repack_full_response))
+
+ client.repack_full(true)
+ end
+ end
+
+ describe '#repack_incremental' do
+ it 'sends a repack_incremental message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:repack_incremental)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double(:repack_incremental_response))
+
+ client.repack_incremental
+ end
+ end
+
+ describe '#repository_size' do
+ it 'sends a repository_size message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:repository_size)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(size: 0)
+
+ client.repository_size
+ end
+ end
+
+ describe '#apply_gitattributes' do
+ let(:revision) { 'master' }
+
+ it 'sends an apply_gitattributes message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:apply_gitattributes)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double(:apply_gitattributes_response))
+
+ client.apply_gitattributes(revision)
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 921e786a55c..a9b861fcff2 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -102,6 +102,22 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do
expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false)
end
end
+
+ context "when a feature is not persisted" do
+ it 'returns false when opt_into_all_features is off' do
+ allow(Feature).to receive(:persisted?).and_return(false)
+ allow(described_class).to receive(:opt_into_all_features?).and_return(false)
+
+ expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false)
+ end
+
+ it 'returns true when the override is on' do
+ allow(Feature).to receive(:persisted?).and_return(false)
+ allow(described_class).to receive(:opt_into_all_features?).and_return(true)
+
+ expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true)
+ end
+ end
end
context 'when the feature_status is OPT_OUT' do
diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
index da48d8f0670..82548c7fd31 100644
--- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
@@ -12,7 +12,7 @@ describe Gitlab::GitlabImport::ProjectCreator do
owner: { name: "john" }
}.with_indifferent_access
end
- let(:namespace){ create(:group, owner: user) }
+ let(:namespace) { create(:group, owner: user) }
let(:token) { "asdffg" }
let(:access_params) { { gitlab_access_token: token } }
diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
index aad53938d52..8d5b60d50de 100644
--- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::GoogleCodeImport::ProjectCreator do
"repositoryUrls" => ["https://vim.googlecode.com/git/"]
)
end
- let(:namespace){ create(:group, owner: user) }
+ let(:namespace) { create(:group, owner: user) }
before do
namespace.add_owner(user)
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index ddb8dd9f0f4..b07462e4978 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -1,43 +1,8 @@
require 'rails_helper'
-RSpec.describe Gitlab::Gpg::Commit do
+describe Gitlab::Gpg::Commit do
describe '#signature' do
- let!(:project) { create :project, :repository, path: 'sample-project' }
- let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
-
- context 'unisgned commit' do
- it 'returns nil' do
- expect(described_class.new(project.commit).signature).to be_nil
- end
- end
-
- context 'known and verified public key' do
- let!(:gpg_key) do
- create :gpg_key, key: GpgHelpers::User1.public_key, user: create(:user, email: GpgHelpers::User1.emails.first)
- end
-
- let!(:commit) do
- raw_commit = double(:raw_commit, signature: [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ], sha: commit_sha)
- allow(raw_commit).to receive :save!
-
- create :commit, git_commit: raw_commit, project: project
- 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,
- gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- gpg_key_user_name: GpgHelpers::User1.names.first,
- gpg_key_user_email: GpgHelpers::User1.emails.first,
- valid_signature: true
- )
- end
-
+ shared_examples 'returns the cached signature on second call' do
it 'returns the cached signature on second call' do
gpg_commit = described_class.new(commit)
@@ -50,54 +15,177 @@ RSpec.describe Gitlab::Gpg::Commit do
end
end
- context 'known but unverified public key' do
- let!(:gpg_key) { create :gpg_key, key: GpgHelpers::User1.public_key }
+ let!(:project) { create :project, :repository, path: 'sample-project' }
+ let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
- let!(:commit) do
- raw_commit = double(:raw_commit, signature: [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ], sha: commit_sha)
- allow(raw_commit).to receive :save!
+ context 'unsigned commit' do
+ let!(:commit) { create :commit, project: project, sha: commit_sha }
- create :commit, git_commit: raw_commit, project: project
+ it 'returns nil' do
+ expect(described_class.new(commit).signature).to be_nil
end
+ end
- it 'returns an invalid signature' do
- expect(described_class.new(commit).signature).to have_attributes(
- commit_sha: commit_sha,
- project: project,
- gpg_key: gpg_key,
- gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- gpg_key_user_name: GpgHelpers::User1.names.first,
- gpg_key_user_email: GpgHelpers::User1.emails.first,
- valid_signature: false
- )
+ context 'known key' do
+ context 'user matches the key uid' do
+ context 'user email matches the email committer' do
+ let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first }
+
+ let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
+
+ let!(:gpg_key) do
+ create :gpg_key, key: GpgHelpers::User1.public_key, user: user
+ end
+
+ before 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
+ ]
+ )
+ 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,
+ gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
+ gpg_key_user_name: GpgHelpers::User1.names.first,
+ gpg_key_user_email: GpgHelpers::User1.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 }
+
+ let(:user) do
+ create(:user, email: GpgHelpers::User1.emails.first).tap do |user|
+ create :email, user: user, email: GpgHelpers::User2.emails.first
+ end
+ end
+
+ let!(:gpg_key) do
+ create :gpg_key, key: GpgHelpers::User1.public_key, user: user
+ end
+
+ before 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
+ ]
+ )
+ end
+
+ it 'returns an invalid signature' do
+ expect(described_class.new(commit).signature).to have_attributes(
+ commit_sha: commit_sha,
+ project: project,
+ gpg_key: gpg_key,
+ gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
+ gpg_key_user_name: GpgHelpers::User1.names.first,
+ gpg_key_user_email: GpgHelpers::User1.emails.first,
+ verification_status: 'same_user_different_email'
+ )
+ end
+
+ it_behaves_like 'returns the cached signature on second call'
+ end
+
+ context 'user email does not match the committer email' do
+ let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first }
+
+ let(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
+
+ let!(:gpg_key) do
+ create :gpg_key, key: GpgHelpers::User1.public_key, user: user
+ end
+
+ before 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
+ ]
+ )
+ end
+
+ it 'returns an invalid signature' do
+ expect(described_class.new(commit).signature).to have_attributes(
+ commit_sha: commit_sha,
+ project: project,
+ gpg_key: gpg_key,
+ gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
+ gpg_key_user_name: GpgHelpers::User1.names.first,
+ gpg_key_user_email: GpgHelpers::User1.emails.first,
+ verification_status: 'other_user'
+ )
+ end
+
+ it_behaves_like 'returns the cached signature on second call'
+ end
end
- it 'returns the cached signature on second call' do
- gpg_commit = described_class.new(commit)
-
- expect(gpg_commit).to receive(:using_keychain).and_call_original
- gpg_commit.signature
-
- # consecutive call
- expect(gpg_commit).not_to receive(:using_keychain).and_call_original
- gpg_commit.signature
+ context 'user does not match the key uid' do
+ let!(:commit) { create :commit, project: project, sha: commit_sha }
+
+ let(:user) { create(:user, email: GpgHelpers::User2.emails.first) }
+
+ let!(:gpg_key) do
+ create :gpg_key, key: GpgHelpers::User1.public_key, user: user
+ end
+
+ before 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
+ ]
+ )
+ end
+
+ it 'returns an invalid signature' do
+ expect(described_class.new(commit).signature).to have_attributes(
+ commit_sha: commit_sha,
+ project: project,
+ gpg_key: gpg_key,
+ gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
+ gpg_key_user_name: GpgHelpers::User1.names.first,
+ gpg_key_user_email: GpgHelpers::User1.emails.first,
+ verification_status: 'unverified_key'
+ )
+ end
+
+ it_behaves_like 'returns the cached signature on second call'
end
end
- context 'unknown public key' do
- let!(:commit) do
- raw_commit = double(:raw_commit, signature: [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ], sha: commit_sha)
- allow(raw_commit).to receive :save!
-
- create :commit,
- git_commit: raw_commit,
- project: project
+ context 'unknown key' do
+ let!(:commit) { create :commit, project: project, sha: commit_sha }
+
+ before 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
+ ]
+ )
end
it 'returns an invalid signature' do
@@ -108,20 +196,11 @@ RSpec.describe Gitlab::Gpg::Commit do
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
gpg_key_user_name: nil,
gpg_key_user_email: nil,
- valid_signature: false
+ verification_status: 'unknown_key'
)
end
- it 'returns the cached signature on second call' do
- gpg_commit = described_class.new(commit)
-
- expect(gpg_commit).to receive(:using_keychain).and_call_original
- gpg_commit.signature
-
- # consecutive call
- expect(gpg_commit).not_to receive(:using_keychain).and_call_original
- gpg_commit.signature
- end
+ it_behaves_like 'returns the cached signature on second call'
end
end
end
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 c4e04ee46a2..b9fd4d02156 100644
--- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
+++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
@@ -5,10 +5,15 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
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
- ], sha: commit_sha)
+ raw_commit = double(
+ :raw_commit,
+ signature: [
+ GpgHelpers::User1.signed_commit_signature,
+ GpgHelpers::User1.signed_commit_base_data
+ ],
+ sha: commit_sha,
+ committer_email: GpgHelpers::User1.emails.first
+ )
allow(raw_commit).to receive :save!
@@ -21,6 +26,15 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
before do
allow_any_instance_of(Project).to receive(:commit).and_return(commit)
+
+ 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
+ ]
+ )
end
context 'gpg signature did have an associated gpg key which was removed later' do
@@ -32,7 +46,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- valid_signature: true
+ verification_status: 'verified'
end
it 'assigns the gpg key to the signature when the missing gpg key is added' do
@@ -46,7 +60,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- valid_signature: true
+ verification_status: 'verified'
)
end
@@ -61,7 +75,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- valid_signature: true
+ verification_status: 'verified'
)
end
end
@@ -75,7 +89,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- valid_signature: false
+ verification_status: 'unknown_key'
end
it 'updates the signature to being valid when the missing gpg key is added' do
@@ -89,7 +103,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- valid_signature: true
+ verification_status: 'verified'
)
end
@@ -104,7 +118,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- valid_signature: false
+ verification_status: 'unknown_key'
)
end
end
@@ -122,7 +136,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: nil,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- valid_signature: false
+ verification_status: 'unknown_key'
end
it 'updates the signature to being valid when the user updates the email address' do
@@ -130,7 +144,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
key: GpgHelpers::User1.public_key,
user: user
- expect(invalid_gpg_signature.reload.valid_signature).to be_falsey
+ expect(invalid_gpg_signature.reload.verification_status).to eq 'unverified_key'
# InvalidGpgSignatureUpdater is called by the after_update hook
user.update_attributes!(email: GpgHelpers::User1.emails.first)
@@ -140,7 +154,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- valid_signature: true
+ verification_status: 'verified'
)
end
@@ -154,7 +168,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- valid_signature: false
+ verification_status: 'unverified_key'
)
# InvalidGpgSignatureUpdater is called by the after_update hook
@@ -165,7 +179,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
commit_sha: commit_sha,
gpg_key: gpg_key,
gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid,
- valid_signature: false
+ verification_status: 'unverified_key'
)
end
end
diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb
index 8041518117d..11a2aea1915 100644
--- a/spec/lib/gitlab/gpg_spec.rb
+++ b/spec/lib/gitlab/gpg_spec.rb
@@ -42,6 +42,73 @@ describe Gitlab::Gpg do
described_class.user_infos_from_key('bogus')
).to eq []
end
+
+ it 'downcases the email' do
+ public_key = double(:key)
+ fingerprints = double(:fingerprints)
+ uid = double(:uid, name: 'Nannie Bernhard', email: 'NANNIE.BERNHARD@EXAMPLE.COM')
+ raw_key = double(:raw_key, uids: [uid])
+ allow(Gitlab::Gpg::CurrentKeyChain).to receive(:fingerprints_from_key).with(public_key).and_return(fingerprints)
+ allow(GPGME::Key).to receive(:find).with(:public, anything).and_return([raw_key])
+
+ user_infos = described_class.user_infos_from_key(public_key)
+ expect(user_infos).to eq([{
+ name: 'Nannie Bernhard',
+ email: 'nannie.bernhard@example.com'
+ }])
+ end
+ end
+
+ describe '.current_home_dir' do
+ let(:default_home_dir) { GPGME::Engine.dirinfo('homedir') }
+
+ it 'returns the default value when no explicit home dir has been set' do
+ expect(described_class.current_home_dir).to eq default_home_dir
+ end
+
+ it 'returns the explicitely set home dir' do
+ GPGME::Engine.home_dir = '/tmp/gpg'
+
+ expect(described_class.current_home_dir).to eq '/tmp/gpg'
+
+ GPGME::Engine.home_dir = GPGME::Engine.dirinfo('homedir')
+ end
+
+ it 'returns the default value when explicitely setting the home dir to nil' do
+ GPGME::Engine.home_dir = nil
+
+ expect(described_class.current_home_dir).to eq default_home_dir
+ end
+ end
+
+ describe '.using_tmp_keychain' do
+ it "the second thread does not change the first thread's directory" do
+ thread1 = Thread.new do
+ described_class.using_tmp_keychain do
+ dir = described_class.current_home_dir
+ sleep 0.1
+ expect(described_class.current_home_dir).to eq dir
+ end
+ end
+
+ thread2 = Thread.new do
+ described_class.using_tmp_keychain do
+ sleep 0.2
+ end
+ end
+
+ thread1.join
+ thread2.join
+ end
+
+ it 'allows recursive execution in the same thread' do
+ expect do
+ described_class.using_tmp_keychain do
+ described_class.using_tmp_keychain do
+ end
+ end
+ end.not_to raise_error(ThreadError)
+ 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 26574df8bb5..f5c9680bf59 100644
--- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
@@ -106,12 +106,6 @@ describe Gitlab::HealthChecks::FsShardsCheck do
}.with_indifferent_access
end
- # Unsolved intermittent failure in CI https://gitlab.com/gitlab-org/gitlab-ce/issues/31128
- around(:each) do |example| # rubocop:disable RSpec/AroundBlock
- times_to_try = ENV['CI'] ? 4 : 1
- example.run_with_retry retry: times_to_try
- end
-
it 'provides metrics' do
metrics = described_class.metrics
diff --git a/spec/lib/gitlab/i18n/metadata_entry_spec.rb b/spec/lib/gitlab/i18n/metadata_entry_spec.rb
new file mode 100644
index 00000000000..ab71d6454a9
--- /dev/null
+++ b/spec/lib/gitlab/i18n/metadata_entry_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::I18n::MetadataEntry do
+ describe '#expected_plurals' do
+ it 'returns the number of plurals' do
+ data = {
+ msgid: "",
+ msgstr: [
+ "",
+ "Project-Id-Version: gitlab 1.0.0\\n",
+ "Report-Msgid-Bugs-To: \\n",
+ "PO-Revision-Date: 2017-07-13 12:10-0500\\n",
+ "Language-Team: Spanish\\n",
+ "Language: es\\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",
+ "Last-Translator: Bob Van Landuyt <bob@gitlab.com>\\n",
+ "X-Generator: Poedit 2.0.2\\n"
+ ]
+ }
+ entry = described_class.new(data)
+
+ expect(entry.expected_plurals).to eq(2)
+ end
+
+ it 'returns 0 for the POT-metadata' do
+ data = {
+ msgid: "",
+ msgstr: [
+ "",
+ "Project-Id-Version: gitlab 1.0.0\\n",
+ "Report-Msgid-Bugs-To: \\n",
+ "PO-Revision-Date: 2017-07-13 12:10-0500\\n",
+ "Language-Team: Spanish\\n",
+ "Language: es\\n",
+ "MIME-Version: 1.0\\n",
+ "Content-Type: text/plain; charset=UTF-8\\n",
+ "Content-Transfer-Encoding: 8bit\\n",
+ "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n",
+ "Last-Translator: Bob Van Landuyt <bob@gitlab.com>\\n",
+ "X-Generator: Poedit 2.0.2\\n"
+ ]
+ }
+ entry = described_class.new(data)
+
+ expect(entry.expected_plurals).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/i18n/po_linter_spec.rb b/spec/lib/gitlab/i18n/po_linter_spec.rb
new file mode 100644
index 00000000000..3a962ba7f22
--- /dev/null
+++ b/spec/lib/gitlab/i18n/po_linter_spec.rb
@@ -0,0 +1,338 @@
+require 'spec_helper'
+require 'simple_po_parser'
+
+describe Gitlab::I18n::PoLinter do
+ let(:linter) { described_class.new(po_path) }
+ let(:po_path) { 'spec/fixtures/valid.po' }
+
+ describe '#errors' do
+ it 'only calls validation once' do
+ expect(linter).to receive(:validate_po).once.and_call_original
+
+ 2.times { linter.errors }
+ end
+ end
+
+ describe '#validate_po' do
+ subject(:errors) { linter.validate_po }
+
+ context 'for a fuzzy message' do
+ let(:po_path) { 'spec/fixtures/fuzzy.po' }
+
+ it 'has an error' do
+ is_expected.to include('PipelineSchedules|Remove variable row' => ['is marked fuzzy'])
+ end
+ end
+
+ context 'for a translations with newlines' do
+ let(:po_path) { 'spec/fixtures/newlines.po' }
+
+ it 'has an error for a normal string' do
+ message_id = "You are going to remove %{group_name}.\\nRemoved groups CANNOT be restored!\\nAre you ABSOLUTELY sure?"
+ expected_message = "is defined over multiple lines, this breaks some tooling."
+
+ expect(errors[message_id]).to include(expected_message)
+ end
+
+ it 'has an error when a translation is defined over multiple lines' do
+ message_id = "You are going to remove %{group_name}.\\nRemoved groups CANNOT be restored!\\nAre you ABSOLUTELY sure?"
+ expected_message = "has translations defined over multiple lines, this breaks some tooling."
+
+ expect(errors[message_id]).to include(expected_message)
+ end
+
+ it 'raises an error when a plural translation is defined over multiple lines' do
+ message_id = 'With plural'
+ expected_message = "has translations defined over multiple lines, this breaks some tooling."
+
+ expect(errors[message_id]).to include(expected_message)
+ end
+
+ it 'raises an error when the plural id is defined over multiple lines' do
+ message_id = 'multiline plural id'
+ expected_message = "plural is defined over multiple lines, this breaks some tooling."
+
+ expect(errors[message_id]).to include(expected_message)
+ end
+ end
+
+ context 'with an invalid po' do
+ let(:po_path) { 'spec/fixtures/invalid.po' }
+
+ it 'returns the error' do
+ is_expected.to include('PO-syntax errors' => a_kind_of(Array))
+ end
+
+ it 'does not validate entries' do
+ expect(linter).not_to receive(:validate_entries)
+
+ linter.validate_po
+ end
+ end
+
+ context 'with missing metadata' do
+ let(:po_path) { 'spec/fixtures/missing_metadata.po' }
+
+ it 'returns the an error' do
+ is_expected.to include('PO-syntax errors' => a_kind_of(Array))
+ end
+ end
+
+ context 'with a valid po' do
+ it 'parses the file' do
+ expect(linter).to receive(:parse_po).and_call_original
+
+ linter.validate_po
+ end
+
+ it 'validates the entries' do
+ expect(linter).to receive(:validate_entries).and_call_original
+
+ linter.validate_po
+ end
+
+ it 'has no errors' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'with missing plurals' do
+ let(:po_path) { 'spec/fixtures/missing_plurals.po' }
+
+ it 'has errors' do
+ is_expected.not_to be_empty
+ end
+ end
+
+ context 'with multiple plurals' do
+ let(:po_path) { 'spec/fixtures/multiple_plurals.po' }
+
+ it 'has errors' do
+ is_expected.not_to be_empty
+ end
+ end
+
+ context 'with unescaped chars' do
+ let(:po_path) { 'spec/fixtures/unescaped_chars.po' }
+
+ it 'contains an error' do
+ message_id = 'You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?'
+ expected_error = 'translation contains unescaped `%`, escape it using `%%`'
+
+ expect(errors[message_id]).to include(expected_error)
+ end
+ end
+ end
+
+ describe '#parse_po' do
+ context 'with a valid po' do
+ it 'fills in the entries' do
+ linter.parse_po
+
+ expect(linter.translation_entries).not_to be_empty
+ expect(linter.metadata_entry).to be_kind_of(Gitlab::I18n::MetadataEntry)
+ end
+
+ it 'does not have errors' do
+ expect(linter.parse_po).to be_nil
+ end
+ end
+
+ context 'with an invalid po' do
+ let(:po_path) { 'spec/fixtures/invalid.po' }
+
+ it 'contains an error' do
+ expect(linter.parse_po).not_to be_nil
+ end
+
+ it 'sets the entries to an empty array' do
+ linter.parse_po
+
+ expect(linter.translation_entries).to eq([])
+ end
+ end
+ end
+
+ describe '#validate_entries' do
+ it 'keeps track of errors for entries' do
+ fake_invalid_entry = Gitlab::I18n::TranslationEntry.new(
+ { msgid: "Hello %{world}", msgstr: "Bonjour %{monde}" }, 2
+ )
+ allow(linter).to receive(:translation_entries) { [fake_invalid_entry] }
+
+ expect(linter).to receive(:validate_entry)
+ .with(fake_invalid_entry)
+ .and_call_original
+
+ expect(linter.validate_entries).to include("Hello %{world}" => an_instance_of(Array))
+ end
+ end
+
+ describe '#validate_entry' do
+ it 'validates the flags, variable usage, newlines, and unescaped chars' do
+ fake_entry = double
+
+ expect(linter).to receive(:validate_flags).with([], fake_entry)
+ expect(linter).to receive(:validate_variables).with([], fake_entry)
+ expect(linter).to receive(:validate_newlines).with([], fake_entry)
+ expect(linter).to receive(:validate_number_of_plurals).with([], fake_entry)
+ expect(linter).to receive(:validate_unescaped_chars).with([], fake_entry)
+
+ linter.validate_entry(fake_entry)
+ end
+ end
+
+ describe '#validate_number_of_plurals' do
+ it 'validates when there are an incorrect number of translations' do
+ fake_metadata = double
+ allow(fake_metadata).to receive(:expected_plurals).and_return(2)
+ allow(linter).to receive(:metadata_entry).and_return(fake_metadata)
+
+ fake_entry = Gitlab::I18n::TranslationEntry.new(
+ { msgid: 'the singular', msgid_plural: 'the plural', 'msgstr[0]' => 'the singular' },
+ 2
+ )
+ errors = []
+
+ linter.validate_number_of_plurals(errors, fake_entry)
+
+ expect(errors).to include('should have 2 translations')
+ end
+ end
+
+ describe '#validate_variables' do
+ it 'validates both signular and plural in a pluralized string when the entry has a singular' do
+ pluralized_entry = Gitlab::I18n::TranslationEntry.new(
+ { msgid: 'Hello %{world}',
+ msgid_plural: 'Hello all %{world}',
+ 'msgstr[0]' => 'Bonjour %{world}',
+ 'msgstr[1]' => 'Bonjour tous %{world}' },
+ 2
+ )
+
+ expect(linter).to receive(:validate_variables_in_message)
+ .with([], 'Hello %{world}', 'Bonjour %{world}')
+ .and_call_original
+ expect(linter).to receive(:validate_variables_in_message)
+ .with([], 'Hello all %{world}', 'Bonjour tous %{world}')
+ .and_call_original
+
+ linter.validate_variables([], pluralized_entry)
+ end
+
+ it 'only validates plural when there is no separate singular' do
+ pluralized_entry = Gitlab::I18n::TranslationEntry.new(
+ { msgid: 'Hello %{world}',
+ msgid_plural: 'Hello all %{world}',
+ 'msgstr[0]' => 'Bonjour %{world}' },
+ 1
+ )
+
+ expect(linter).to receive(:validate_variables_in_message)
+ .with([], 'Hello all %{world}', 'Bonjour %{world}')
+
+ linter.validate_variables([], pluralized_entry)
+ end
+
+ it 'validates the message variables' do
+ entry = Gitlab::I18n::TranslationEntry.new(
+ { msgid: 'Hello', msgstr: 'Bonjour' },
+ 2
+ )
+
+ expect(linter).to receive(:validate_variables_in_message)
+ .with([], 'Hello', 'Bonjour')
+
+ linter.validate_variables([], entry)
+ end
+ end
+
+ describe '#validate_variables_in_message' do
+ it 'detects when a variables are used incorrectly' do
+ errors = []
+
+ expected_errors = ['<hello %{world} %d> is missing: [%{hello}]',
+ '<hello %{world} %d> is using unknown variables: [%{world}]',
+ 'is combining multiple unnamed variables']
+
+ linter.validate_variables_in_message(errors, '%{hello} world %d', 'hello %{world} %d')
+
+ expect(errors).to include(*expected_errors)
+ end
+ end
+
+ describe '#validate_translation' do
+ it 'succeeds with valid variables' do
+ errors = []
+
+ linter.validate_translation(errors, 'Hello %{world}', ['%{world}'])
+
+ expect(errors).to be_empty
+ end
+
+ it 'adds an error message when translating fails' do
+ errors = []
+
+ expect(FastGettext::Translation).to receive(:_) { raise 'broken' }
+
+ linter.validate_translation(errors, 'Hello', [])
+
+ expect(errors).to include('Failure translating to en with []: broken')
+ end
+
+ it 'adds an error message when translating fails when translating with context' do
+ errors = []
+
+ expect(FastGettext::Translation).to receive(:s_) { raise 'broken' }
+
+ linter.validate_translation(errors, 'Tests|Hello', [])
+
+ expect(errors).to include('Failure translating to en with []: broken')
+ end
+
+ it "adds an error when trying to translate with incorrect variables when using unnamed variables" do
+ errors = []
+
+ linter.validate_translation(errors, 'Hello %d', ['%s'])
+
+ expect(errors.first).to start_with("Failure translating to en with")
+ end
+
+ it "adds an error when trying to translate with named variables when unnamed variables are expected" do
+ errors = []
+
+ linter.validate_translation(errors, 'Hello %d', ['%{world}'])
+
+ expect(errors.first).to start_with("Failure translating to en with")
+ end
+
+ it 'adds an error when translated with incorrect variables using named variables' do
+ errors = []
+
+ linter.validate_translation(errors, 'Hello %{thing}', ['%d'])
+
+ expect(errors.first).to start_with("Failure translating to en with")
+ end
+ end
+
+ describe '#fill_in_variables' do
+ it 'builds an array for %d translations' do
+ result = linter.fill_in_variables(['%d'])
+
+ expect(result).to contain_exactly(a_kind_of(Integer))
+ end
+
+ it 'builds an array for %s translations' do
+ result = linter.fill_in_variables(['%s'])
+
+ expect(result).to contain_exactly(a_kind_of(String))
+ end
+
+ it 'builds a hash for named variables' do
+ result = linter.fill_in_variables(['%{hello}'])
+
+ expect(result).to be_a(Hash)
+ expect(result).to include('hello' => an_instance_of(String))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/i18n/translation_entry_spec.rb b/spec/lib/gitlab/i18n/translation_entry_spec.rb
new file mode 100644
index 00000000000..f68bc8feff9
--- /dev/null
+++ b/spec/lib/gitlab/i18n/translation_entry_spec.rb
@@ -0,0 +1,203 @@
+require 'spec_helper'
+
+describe Gitlab::I18n::TranslationEntry do
+ describe '#singular_translation' do
+ it 'returns the normal `msgstr` for translations without plural' do
+ data = { msgid: 'Hello world', msgstr: 'Bonjour monde' }
+ entry = described_class.new(data, 2)
+
+ expect(entry.singular_translation).to eq('Bonjour monde')
+ end
+
+ it 'returns the first string for entries with plurals' do
+ data = {
+ msgid: 'Hello world',
+ msgid_plural: 'Hello worlds',
+ 'msgstr[0]' => 'Bonjour monde',
+ 'msgstr[1]' => 'Bonjour mondes'
+ }
+ entry = described_class.new(data, 2)
+
+ expect(entry.singular_translation).to eq('Bonjour monde')
+ end
+ end
+
+ describe '#all_translations' do
+ it 'returns all translations for singular translations' do
+ data = { msgid: 'Hello world', msgstr: 'Bonjour monde' }
+ entry = described_class.new(data, 2)
+
+ expect(entry.all_translations).to eq(['Bonjour monde'])
+ end
+
+ it 'returns all translations when including plural translations' do
+ data = {
+ msgid: 'Hello world',
+ msgid_plural: 'Hello worlds',
+ 'msgstr[0]' => 'Bonjour monde',
+ 'msgstr[1]' => 'Bonjour mondes'
+ }
+ entry = described_class.new(data, 2)
+
+ expect(entry.all_translations).to eq(['Bonjour monde', 'Bonjour mondes'])
+ end
+ end
+
+ describe '#plural_translations' do
+ it 'returns all translations if there is only one plural' do
+ data = {
+ msgid: 'Hello world',
+ msgid_plural: 'Hello worlds',
+ 'msgstr[0]' => 'Bonjour monde'
+ }
+ entry = described_class.new(data, 1)
+
+ expect(entry.plural_translations).to eq(['Bonjour monde'])
+ end
+
+ it 'returns all translations except for the first one if there are multiple' do
+ data = {
+ msgid: 'Hello world',
+ msgid_plural: 'Hello worlds',
+ 'msgstr[0]' => 'Bonjour monde',
+ 'msgstr[1]' => 'Bonjour mondes',
+ 'msgstr[2]' => 'Bonjour tous les mondes'
+ }
+ entry = described_class.new(data, 3)
+
+ expect(entry.plural_translations).to eq(['Bonjour mondes', 'Bonjour tous les mondes'])
+ end
+ end
+
+ describe '#has_singular_translation?' do
+ it 'has a singular when the translation is not pluralized' do
+ data = {
+ msgid: 'hello world',
+ msgstr: 'hello'
+ }
+ entry = described_class.new(data, 2)
+
+ expect(entry).to have_singular_translation
+ end
+
+ it 'has a singular when plural and singular are separately defined' do
+ data = {
+ msgid: 'hello world',
+ msgid_plural: 'hello worlds',
+ "msgstr[0]" => 'hello world',
+ "msgstr[1]" => 'hello worlds'
+ }
+ entry = described_class.new(data, 2)
+
+ expect(entry).to have_singular_translation
+ end
+
+ it 'does not have a separate singular if the plural string only has one translation' do
+ data = {
+ msgid: 'hello world',
+ msgid_plural: 'hello worlds',
+ "msgstr[0]" => 'hello worlds'
+ }
+ entry = described_class.new(data, 1)
+
+ expect(entry).not_to have_singular_translation
+ end
+ end
+
+ describe '#msgid_contains_newlines' do
+ it 'is true when the msgid is an array' do
+ data = { msgid: %w(hello world) }
+ entry = described_class.new(data, 2)
+
+ expect(entry.msgid_contains_newlines?).to be_truthy
+ end
+ end
+
+ describe '#plural_id_contains_newlines' do
+ it 'is true when the msgid is an array' do
+ data = { msgid_plural: %w(hello world) }
+ entry = described_class.new(data, 2)
+
+ expect(entry.plural_id_contains_newlines?).to be_truthy
+ end
+ end
+
+ describe '#translations_contain_newlines' do
+ it 'is true when the msgid is an array' do
+ data = { msgstr: %w(hello world) }
+ entry = described_class.new(data, 2)
+
+ expect(entry.translations_contain_newlines?).to be_truthy
+ end
+ end
+
+ describe '#contains_unescaped_chars' do
+ let(:data) { { msgid: '' } }
+ let(:entry) { described_class.new(data, 2) }
+ it 'is true when the msgid is an array' do
+ string = '「100%確定ã€'
+
+ expect(entry.contains_unescaped_chars?(string)).to be_truthy
+ end
+
+ it 'is false when the `%` char is escaped' do
+ string = '「100%%確定ã€'
+
+ expect(entry.contains_unescaped_chars?(string)).to be_falsy
+ end
+
+ it 'is false when using an unnamed variable' do
+ string = '「100%d確定ã€'
+
+ expect(entry.contains_unescaped_chars?(string)).to be_falsy
+ end
+
+ it 'is false when using a named variable' do
+ string = '「100%{named}確定ã€'
+
+ expect(entry.contains_unescaped_chars?(string)).to be_falsy
+ end
+
+ it 'is true when an unnamed variable is not closed' do
+ string = '「100%{named確定ã€'
+
+ expect(entry.contains_unescaped_chars?(string)).to be_truthy
+ end
+
+ it 'is true when the string starts with a `%`' do
+ string = '%10'
+
+ expect(entry.contains_unescaped_chars?(string)).to be_truthy
+ end
+ end
+
+ describe '#msgid_contains_unescaped_chars' do
+ it 'is true when the msgid contains a `%`' do
+ data = { msgid: '「100%確定ã€' }
+ entry = described_class.new(data, 2)
+
+ expect(entry).to receive(:contains_unescaped_chars?).and_call_original
+ expect(entry.msgid_contains_unescaped_chars?).to be_truthy
+ end
+ end
+
+ describe '#plural_id_contains_unescaped_chars' do
+ it 'is true when the plural msgid contains a `%`' do
+ data = { msgid_plural: '「100%確定ã€' }
+ entry = described_class.new(data, 2)
+
+ expect(entry).to receive(:contains_unescaped_chars?).and_call_original
+ expect(entry.plural_id_contains_unescaped_chars?).to be_truthy
+ end
+ end
+
+ describe '#translations_contain_unescaped_chars' do
+ it 'is true when the translation contains a `%`' do
+ data = { msgstr: '「100%確定ã€' }
+ entry = described_class.new(data, 2)
+
+ expect(entry).to receive(:contains_unescaped_chars?).and_call_original
+ expect(entry.translations_contain_unescaped_chars?).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 6a41afe0c25..3fb8edeb701 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -22,6 +22,7 @@ events:
- author
- project
- target
+- push_event_payload
notes:
- award_emoji
- project
@@ -255,6 +256,7 @@ project:
- environments
- deployments
- project_feature
+- auto_devops
- pages_domains
- authorized_users
- project_authorizations
@@ -263,6 +265,7 @@ project:
- statistics
- container_repositories
- uploads
+- members_and_requesters
award_emoji:
- awardable
- user
@@ -272,3 +275,5 @@ timelogs:
- issue
- merge_request
- user
+push_event_payload:
+- event
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
index 574748756bd..cd5a1b2982b 100644
--- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::ImportExport::AttributeCleaner do
- let(:relation_class){ double('relation_class').as_null_object }
+ let(:relation_class) { double('relation_class').as_null_object }
let(:unsafe_hash) do
{
'id' => 101,
diff --git a/spec/lib/gitlab/import_export/file_importer_spec.rb b/spec/lib/gitlab/import_export/file_importer_spec.rb
index 690c7625c52..162b776e107 100644
--- a/spec/lib/gitlab/import_export/file_importer_spec.rb
+++ b/spec/lib/gitlab/import_export/file_importer_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::ImportExport::FileImporter do
let(:export_path) { "#{Dir.tmpdir}/file_importer_spec" }
let(:valid_file) { "#{shared.export_path}/valid.json" }
let(:symlink_file) { "#{shared.export_path}/invalid.json" }
+ let(:hidden_symlink_file) { "#{shared.export_path}/.hidden" }
let(:subfolder_symlink_file) { "#{shared.export_path}/subfolder/invalid.json" }
before do
@@ -25,6 +26,10 @@ describe Gitlab::ImportExport::FileImporter do
expect(File.exist?(symlink_file)).to be false
end
+ it 'removes hidden symlinks in root folder' do
+ expect(File.exist?(hidden_symlink_file)).to be false
+ end
+
it 'removes symlinks in subfolders' do
expect(File.exist?(subfolder_symlink_file)).to be false
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 4e631e13410..1115fb218d6 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -75,8 +75,6 @@
"id": 487,
"target_type": "Milestone",
"target_id": 1,
- "title": null,
- "data": null,
"project_id": 46,
"created_at": "2016-06-14T15:02:04.418Z",
"updated_at": "2016-06-14T15:02:04.418Z",
@@ -364,8 +362,6 @@
"id": 487,
"target_type": "Milestone",
"target_id": 1,
- "title": null,
- "data": null,
"project_id": 46,
"created_at": "2016-06-14T15:02:04.418Z",
"updated_at": "2016-06-14T15:02:04.418Z",
@@ -2311,8 +2307,6 @@
"id": 487,
"target_type": "Milestone",
"target_id": 1,
- "title": null,
- "data": null,
"project_id": 46,
"created_at": "2016-06-14T15:02:04.418Z",
"updated_at": "2016-06-14T15:02:04.418Z",
@@ -2336,8 +2330,6 @@
"id": 240,
"target_type": "Milestone",
"target_id": 20,
- "title": null,
- "data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:04.593Z",
"updated_at": "2016-06-14T15:02:04.593Z",
@@ -2348,8 +2340,6 @@
"id": 60,
"target_type": "Milestone",
"target_id": 20,
- "title": null,
- "data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:04.593Z",
"updated_at": "2016-06-14T15:02:04.593Z",
@@ -2373,8 +2363,6 @@
"id": 241,
"target_type": "Milestone",
"target_id": 19,
- "title": null,
- "data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:04.585Z",
"updated_at": "2016-06-14T15:02:04.585Z",
@@ -2385,41 +2373,6 @@
"id": 59,
"target_type": "Milestone",
"target_id": 19,
- "title": null,
- "data": {
- "object_kind": "push",
- "before": "0000000000000000000000000000000000000000",
- "after": "de990aa15829d0ab182ad5a55b4c527846c0d39c",
- "ref": "refs/heads/removable-group-owner",
- "checkout_sha": "de990aa15829d0ab182ad5a55b4c527846c0d39c",
- "message": null,
- "user_id": 273486,
- "user_name": "James Lopez",
- "user_email": "james@jameslopez.es",
- "project_id": 562317,
- "repository": {
- "name": "GitLab Community Edition",
- "url": "git@gitlab.com:james11/gitlab-ce.git",
- "description": "Version Control on your Server. See http://gitlab.org/gitlab-ce/ and the README for more information",
- "homepage": "https://gitlab.com/james11/gitlab-ce",
- "git_http_url": "https://gitlab.com/james11/gitlab-ce.git",
- "git_ssh_url": "git@gitlab.com:james11/gitlab-ce.git",
- "visibility_level": 20
- },
- "commits": [
- {
- "id": "de990aa15829d0ab182ad5a55b4c527846c0d39c",
- "message": "fixed last group owner issue and added test\\n",
- "timestamp": "2015-10-29T16:10:27+00:00",
- "url": "https://gitlab.com/james11/gitlab-ce/commit/de990aa15829d0ab182ad5a55b4c527846c0d39c",
- "author": {
- "name": "James Lopez",
- "email": "james.lopez@vodafone.com"
- }
- }
- ],
- "total_commits_count": 1
- },
"project_id": 5,
"created_at": "2016-06-14T15:02:04.585Z",
"updated_at": "2016-06-14T15:02:04.585Z",
@@ -2522,7 +2475,7 @@
"id": 27,
"target_branch": "feature",
"source_branch": "feature_conflict",
- "source_project_id": 5,
+ "source_project_id": 999,
"author_id": 1,
"assignee_id": null,
"title": "MR1",
@@ -2536,6 +2489,9 @@
"position": 0,
"updated_by_id": null,
"merge_error": null,
+ "diff_head_sha": "HEAD",
+ "source_branch_sha": "ABCD",
+ "target_branch_sha": "DCBA",
"merge_params": {
"force_remove_source_branch": null
},
@@ -2944,8 +2900,6 @@
"id": 221,
"target_type": "MergeRequest",
"target_id": 27,
- "title": null,
- "data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:36.703Z",
"updated_at": "2016-06-14T15:02:36.703Z",
@@ -2956,8 +2910,6 @@
"id": 187,
"target_type": "MergeRequest",
"target_id": 27,
- "title": null,
- "data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:36.703Z",
"updated_at": "2016-06-14T15:02:36.703Z",
@@ -3227,8 +3179,6 @@
"id": 222,
"target_type": "MergeRequest",
"target_id": 26,
- "title": null,
- "data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:36.496Z",
"updated_at": "2016-06-14T15:02:36.496Z",
@@ -3239,8 +3189,6 @@
"id": 186,
"target_type": "MergeRequest",
"target_id": 26,
- "title": null,
- "data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:36.496Z",
"updated_at": "2016-06-14T15:02:36.496Z",
@@ -3510,8 +3458,6 @@
"id": 223,
"target_type": "MergeRequest",
"target_id": 15,
- "title": null,
- "data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:25.262Z",
"updated_at": "2016-06-14T15:02:25.262Z",
@@ -3522,8 +3468,6 @@
"id": 175,
"target_type": "MergeRequest",
"target_id": 15,
- "title": null,
- "data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:25.262Z",
"updated_at": "2016-06-14T15:02:25.262Z",
@@ -4199,8 +4143,6 @@
"id": 224,
"target_type": "MergeRequest",
"target_id": 14,
- "title": null,
- "data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:25.113Z",
"updated_at": "2016-06-14T15:02:25.113Z",
@@ -4211,8 +4153,6 @@
"id": 174,
"target_type": "MergeRequest",
"target_id": 14,
- "title": null,
- "data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:25.113Z",
"updated_at": "2016-06-14T15:02:25.113Z",
@@ -4271,9 +4211,7 @@
{
"id": 529,
"target_type": "Note",
- "target_id": 2521,
- "title": "test levels",
- "data": null,
+ "target_id": 793,
"project_id": 4,
"created_at": "2016-07-07T14:35:12.128Z",
"updated_at": "2016-07-07T14:35:12.128Z",
@@ -4746,8 +4684,6 @@
"id": 225,
"target_type": "MergeRequest",
"target_id": 13,
- "title": null,
- "data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:24.636Z",
"updated_at": "2016-06-14T15:02:24.636Z",
@@ -4758,8 +4694,6 @@
"id": 173,
"target_type": "MergeRequest",
"target_id": 13,
- "title": null,
- "data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:24.636Z",
"updated_at": "2016-06-14T15:02:24.636Z",
@@ -5244,8 +5178,6 @@
"id": 226,
"target_type": "MergeRequest",
"target_id": 12,
- "title": null,
- "data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:24.253Z",
"updated_at": "2016-06-14T15:02:24.253Z",
@@ -5256,8 +5188,6 @@
"id": 172,
"target_type": "MergeRequest",
"target_id": 12,
- "title": null,
- "data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:24.253Z",
"updated_at": "2016-06-14T15:02:24.253Z",
@@ -5503,8 +5433,6 @@
"id": 227,
"target_type": "MergeRequest",
"target_id": 11,
- "title": null,
- "data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:23.865Z",
"updated_at": "2016-06-14T15:02:23.865Z",
@@ -5515,8 +5443,6 @@
"id": 171,
"target_type": "MergeRequest",
"target_id": 11,
- "title": null,
- "data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:23.865Z",
"updated_at": "2016-06-14T15:02:23.865Z",
@@ -6192,8 +6118,6 @@
"id": 228,
"target_type": "MergeRequest",
"target_id": 10,
- "title": null,
- "data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:23.660Z",
"updated_at": "2016-06-14T15:02:23.660Z",
@@ -6204,8 +6128,6 @@
"id": 170,
"target_type": "MergeRequest",
"target_id": 10,
- "title": null,
- "data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:23.660Z",
"updated_at": "2016-06-14T15:02:23.660Z",
@@ -6475,8 +6397,6 @@
"id": 229,
"target_type": "MergeRequest",
"target_id": 9,
- "title": null,
- "data": null,
"project_id": 36,
"created_at": "2016-06-14T15:02:22.927Z",
"updated_at": "2016-06-14T15:02:22.927Z",
@@ -6487,8 +6407,6 @@
"id": 169,
"target_type": "MergeRequest",
"target_id": 9,
- "title": null,
- "data": null,
"project_id": 5,
"created_at": "2016-06-14T15:02:22.927Z",
"updated_at": "2016-06-14T15:02:22.927Z",
diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json
index a78836c3c34..2d8f3d4a566 100644
--- a/spec/lib/gitlab/import_export/project.light.json
+++ b/spec/lib/gitlab/import_export/project.light.json
@@ -2,6 +2,20 @@
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
"visibility_level": 10,
"archived": false,
+ "milestones": [
+ {
+ "id": 1,
+ "title": "test milestone",
+ "project_id": 8,
+ "description": "test milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ }
+ ],
"labels": [
{
"id": 2,
@@ -14,20 +28,6 @@
"description": "",
"type": "ProjectLabel",
"priorities": [
- ]
- },
- {
- "id": 3,
- "title": "test3",
- "color": "#428bca",
- "group_id": 8,
- "created_at": "2016-07-22T08:55:44.161Z",
- "updated_at": "2016-07-22T08:55:44.161Z",
- "template": false,
- "description": "",
- "project_id": null,
- "type": "GroupLabel",
- "priorities": [
{
"id": 1,
"project_id": 5,
@@ -39,10 +39,80 @@
]
}
],
+ "issues": [
+ {
+ "id": 1,
+ "title": "Fugiat est minima quae maxime non similique.",
+ "assignee_id": null,
+ "project_id": 8,
+ "author_id": 1,
+ "created_at": "2017-07-07T18:13:01.138Z",
+ "updated_at": "2017-08-15T18:37:40.807Z",
+ "branch_name": null,
+ "description": "Quam totam fuga numquam in eveniet.",
+ "state": "opened",
+ "iid": 20,
+ "updated_by_id": 1,
+ "confidential": false,
+ "deleted_at": null,
+ "due_date": null,
+ "moved_to_id": null,
+ "lock_version": null,
+ "time_estimate": 0,
+ "closed_at": null,
+ "last_edited_at": null,
+ "last_edited_by_id": null,
+ "group_milestone_id": null,
+ "label_links": [
+ {
+ "id": 11,
+ "label_id": 6,
+ "target_id": 1,
+ "target_type": "Issue",
+ "created_at": "2017-08-15T18:37:40.795Z",
+ "updated_at": "2017-08-15T18:37:40.795Z",
+ "label": {
+ "id": 6,
+ "title": "group label",
+ "color": "#A8D695",
+ "project_id": null,
+ "created_at": "2017-08-15T18:37:19.698Z",
+ "updated_at": "2017-08-15T18:37:19.698Z",
+ "template": false,
+ "description": "",
+ "group_id": 5,
+ "type": "GroupLabel",
+ "priorities": []
+ }
+ },
+ {
+ "id": 11,
+ "label_id": 2,
+ "target_id": 1,
+ "target_type": "Issue",
+ "created_at": "2017-08-15T18:37:40.795Z",
+ "updated_at": "2017-08-15T18:37:40.795Z",
+ "label": {
+ "id": 6,
+ "title": "project label",
+ "color": "#A8D695",
+ "project_id": null,
+ "created_at": "2017-08-15T18:37:19.698Z",
+ "updated_at": "2017-08-15T18:37:19.698Z",
+ "template": false,
+ "description": "",
+ "group_id": 5,
+ "type": "ProjectLabel",
+ "priorities": []
+ }
+ }
+ ]
+ }
+ ],
"snippets": [
],
"hooks": [
]
-} \ No newline at end of file
+}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 7ee0e22f28d..efe11ca794a 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -10,6 +10,13 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
@shared = Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path')
allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
@project = create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project')
+
+ allow_any_instance_of(Repository).to receive(:fetch_ref).and_return(true)
+ allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false)
+
+ expect_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch).with('feature', 'DCBA')
+ allow_any_instance_of(Gitlab::Git::Repository).to receive(:create_branch)
+
project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project)
@restored_project_json = project_tree_restorer.restore
end
@@ -50,10 +57,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(Ci::Pipeline.where(ref: nil)).not_to be_empty
end
- it 'restores the correct event with symbolised data' do
- expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty
- end
-
it 'preserves updated_at on issues' do
issue = Issue.where(description: 'Aliquam enim illo et possimus.').first
@@ -73,12 +76,16 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
context 'event at forth level of the tree' do
- let(:event) { Event.where(title: 'test levels').first }
+ let(:event) { Event.where(action: 6).first }
it 'restores the event' do
expect(event).not_to be_nil
end
+ it 'has the action' do
+ expect(event.action).not_to be_nil
+ end
+
it 'event belongs to note, belongs to merge request, belongs to a project' do
expect(event.note.noteable.project).not_to be_nil
end
@@ -183,7 +190,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
let(:restored_project_json) { project_tree_restorer.restore }
before do
- allow(ImportExport).to receive(:project_filename).and_return('project.light.json')
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+
allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
end
@@ -195,7 +203,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
restored_project_json
- expect(shared.errors.first).not_to include('test')
+ expect(shared.errors.first).to be_nil
end
end
end
@@ -219,15 +227,42 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
before do
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+
restored_project_json
end
- it 'has group labels' do
- expect(GroupLabel.count).to eq(1)
+ it 'correctly restores project' do
+ expect(restored_project_json).to be_truthy
+ expect(shared.errors).to be_empty
+ end
+
+ it 'has labels' do
+ expect(project.labels.count).to eq(2)
+ end
+
+ it 'creates group label' do
+ expect(project.group.labels.count).to eq(1)
end
it 'has label priorities' do
- expect(GroupLabel.first.priorities).not_to be_empty
+ expect(project.labels.first.priorities).not_to be_empty
+ end
+
+ it 'has milestones' do
+ expect(project.milestones.count).to eq(1)
+ end
+
+ it 'has issue' do
+ expect(project.issues.count).to eq(1)
+ expect(project.issues.first.labels.count).to eq(2)
+ end
+
+ it 'has issue with group label and project label' do
+ labels = project.issues.first.labels
+
+ expect(labels.where(type: "GroupLabel").count).to eq(1)
+ expect(labels.where(type: "ProjectLabel").count).to eq(1)
end
end
end
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 a278f89c1a1..8e3554375e8 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -11,6 +11,8 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
before do
project.team << [user, :master]
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ allow_any_instance_of(MergeRequest).to receive(:source_branch_sha).and_return('ABCD')
+ allow_any_instance_of(MergeRequest).to receive(:target_branch_sha).and_return('DCBA')
end
after do
@@ -43,6 +45,14 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['merge_requests'].first['milestone']).not_to be_empty
end
+ it 'has merge request\'s source branch SHA' do
+ expect(saved_project_json['merge_requests'].first['source_branch_sha']).to eq('ABCD')
+ end
+
+ it 'has merge request\'s target branch SHA' do
+ expect(saved_project_json['merge_requests'].first['target_branch_sha']).to eq('DCBA')
+ end
+
it 'has events' do
expect(saved_project_json['merge_requests'].first['milestone']['events']).not_to be_empty
end
@@ -107,6 +117,13 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['pipelines'].first['statuses'].count { |hash| hash['type'] == 'Ci::Build' }).to eq(1)
end
+ 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)
+
+ saved_project_json
+ end
+
it 'has pipeline commits' do
expect(saved_project_json['pipelines']).not_to be_empty
end
@@ -241,15 +258,11 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
create(:label_priority, label: group_label, priority: 1)
milestone = create(:milestone, project: project)
merge_request = create(:merge_request, source_project: project, milestone: milestone)
- commit_status = create(:commit_status, project: project)
- ci_pipeline = create(:ci_pipeline,
- project: project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch,
- statuses: [commit_status])
+ ci_build = create(:ci_build, project: project, when: nil)
+ ci_build.pipeline.update(project: project)
+ create(:commit_status, project: project, pipeline: ci_build.pipeline)
- create(:ci_build, pipeline: ci_pipeline, project: project)
create(:milestone, project: project)
create(:note, noteable: issue, project: project)
create(:note, noteable: merge_request, project: project)
@@ -257,7 +270,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
create(:note_on_commit,
author: user,
project: project,
- commit_id: ci_pipeline.sha)
+ commit_id: ci_build.pipeline.sha)
create(:event, :created, target: milestone, project: project, author: user)
create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index 2786bc92fe5..c49af602a01 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -34,7 +34,7 @@ describe Gitlab::ImportExport::RepoRestorer do
it 'has the webhooks' do
restorer.restore
- expect(Gitlab::Git::Hook.new('post-receive', project)).to exist
+ expect(Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository)).to exist
end
end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 2e0d6d346de..96c70d1b675 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -29,13 +29,19 @@ Event:
- id
- target_type
- target_id
-- title
-- data
- project_id
- created_at
- updated_at
- action
- author_id
+PushEventPayload:
+- commit_count
+- action
+- ref_type
+- commit_from
+- commit_to
+- ref
+- commit_title
Note:
- id
- note
@@ -57,6 +63,7 @@ Note:
- change_position
- resolved_at
- resolved_by_id
+- resolved_by_push
- discussion_id
- original_discussion_id
LabelLink:
@@ -216,9 +223,13 @@ Ci::Pipeline:
- lock_version
- auto_canceled_by_id
- pipeline_schedule_id
+- config_source
+- protected
Ci::Stage:
- id
- name
+- status
+- lock_version
- project_id
- pipeline_id
- created_at
@@ -268,6 +279,8 @@ CommitStatus:
- coverage_regex
- auto_canceled_by_id
- retried
+- protected
+- failure_reason
Ci::Variable:
- id
- project_id
@@ -387,6 +400,7 @@ Project:
- public_builds
- last_repository_check_failed
- last_repository_check_at
+- collapse_outdated_diff_comments
- container_registry_enabled
- only_allow_merge_if_pipeline_succeeds
- has_external_issue_tracker
@@ -395,6 +409,7 @@ Project:
- only_allow_merge_if_all_discussions_are_resolved
- auto_cancel_pending_pipelines
- printing_merge_request_link_enabled
+- resolve_outdated_diff_discussions
- build_allow_git_fetch
- last_repository_updated_at
- ci_config_path
@@ -454,3 +469,10 @@ Timelog:
- user_id
- created_at
- updated_at
+ProjectAutoDevops:
+- id
+- enabled
+- domain
+- project_id
+- created_at
+- updated_at
diff --git a/spec/lib/gitlab/issuables_count_for_state_spec.rb b/spec/lib/gitlab/issuables_count_for_state_spec.rb
new file mode 100644
index 00000000000..c262fdfcb61
--- /dev/null
+++ b/spec/lib/gitlab/issuables_count_for_state_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::IssuablesCountForState do
+ let(:finder) do
+ double(:finder, count_by_state: { opened: 2, closed: 1 })
+ end
+
+ let(:counter) { described_class.new(finder) }
+
+ describe '#for_state_or_opened' do
+ it 'returns the number of issuables for the given state' do
+ expect(counter.for_state_or_opened(:closed)).to eq(1)
+ end
+
+ it 'returns the number of open issuables when no state is given' do
+ expect(counter.for_state_or_opened).to eq(2)
+ end
+
+ it 'returns the number of open issuables when a nil value is given' do
+ expect(counter.for_state_or_opened(nil)).to eq(2)
+ end
+ end
+
+ describe '#[]' do
+ it 'returns the number of issuables for the given state' do
+ expect(counter[:closed]).to eq(1)
+ end
+
+ it 'casts valid states from Strings to Symbols' do
+ expect(counter['closed']).to eq(1)
+ end
+
+ it 'returns 0 when using an invalid state name as a String' do
+ expect(counter['kittens']).to be_zero
+ end
+ end
+end
diff --git a/spec/lib/gitlab/job_waiter_spec.rb b/spec/lib/gitlab/job_waiter_spec.rb
index 6186cec2689..b0b4fdc09bc 100644
--- a/spec/lib/gitlab/job_waiter_spec.rb
+++ b/spec/lib/gitlab/job_waiter_spec.rb
@@ -1,30 +1,39 @@
require 'spec_helper'
describe Gitlab::JobWaiter do
- describe '#wait' do
- let(:waiter) { described_class.new(%w(a)) }
- it 'returns when all jobs have been completed' do
- expect(Gitlab::SidekiqStatus).to receive(:all_completed?).with(%w(a))
- .and_return(true)
+ describe '.notify' do
+ it 'pushes the jid to the named queue' do
+ key = 'gitlab:job_waiter:foo'
+ jid = 1
- expect(waiter).not_to receive(:sleep)
+ redis = double('redis')
+ expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
+ expect(redis).to receive(:lpush).with(key, jid)
- waiter.wait
+ described_class.notify(key, jid)
end
+ end
+
+ describe '#wait' do
+ let(:waiter) { described_class.new(2) }
- it 'sleeps between checking the job statuses' do
- expect(Gitlab::SidekiqStatus).to receive(:all_completed?)
- .with(%w(a))
- .and_return(false, true)
+ it 'returns when all jobs have been completed' do
+ described_class.notify(waiter.key, 'a')
+ described_class.notify(waiter.key, 'b')
- expect(waiter).to receive(:sleep).with(described_class::INTERVAL)
+ result = nil
+ expect { Timeout.timeout(1) { result = waiter.wait(2) } }.not_to raise_error
- waiter.wait
+ expect(result).to contain_exactly('a', 'b')
end
- it 'returns when timing out' do
- expect(waiter).not_to receive(:sleep)
- waiter.wait(0)
+ it 'times out if not all jobs complete' do
+ described_class.notify(waiter.key, 'a')
+
+ result = nil
+ expect { Timeout.timeout(2) { result = waiter.wait(1) } }.not_to raise_error
+
+ expect(result).to contain_exactly('a')
end
end
end
diff --git a/spec/lib/gitlab/key_fingerprint_spec.rb b/spec/lib/gitlab/key_fingerprint_spec.rb
deleted file mode 100644
index f5fd5a96bc9..00000000000
--- a/spec/lib/gitlab/key_fingerprint_spec.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::KeyFingerprint, lib: true do
- KEYS = {
- rsa:
- 'example.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC5z65PwQ1GE6foJgwk' \
- '9rmQi/glaXbUeVa5uvQpnZ3Z5+forcI7aTngh3aZ/H2UDP2L70TGy7kKNyp0J3a8/OdG' \
- 'Z08y5yi3JlbjFARO1NyoFEjw2H1SJxeJ43L6zmvTlu+hlK1jSAlidl7enS0ufTlzEEj4' \
- 'iJcuTPKdVzKRgZuTRVm9woWNVKqIrdRC0rJiTinERnfSAp/vNYERMuaoN4oJt8p/NEek' \
- 'rmFoDsQOsyDW5RAnCnjWUU+jFBKDpfkJQ1U2n6BjJewC9dl6ODK639l3yN4WOLZEk4tN' \
- 'UysfbGeF3rmMeflaD6O1Jplpv3YhwVGFNKa7fMq6k3Z0tszTJPYh',
- ecdsa:
- 'example.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAI' \
- 'bmlzdHAyNTYAAABBBKTJy43NZzJSfNxpv/e2E6Zy3qoHoTQbmOsU5FEfpWfWa1MdTeXQ' \
- 'YvKOi+qz/1AaNx6BK421jGu74JCDJtiZWT8=',
- ed25519:
- '@revoked example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjq' \
- 'uxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf',
- dss:
- 'example.com ssh-dss AAAAB3NzaC1kc3MAAACBAP1/U4EddRIpUt9KnC7s5Of2EbdS' \
- 'PO9EAMMeP4C2USZpRV1AIlH7WT2NWPq/xfW6MPbLm1Vs14E7gB00b/JmYLdrmVClpJ+f' \
- '6AR7ECLCT7up1/63xhv4O1fnxqimFQ8E+4P208UewwI1VBNaFpEy9nXzrith1yrv8iID' \
- 'GZ3RSAHHAAAAFQCXYFCPFSMLzLKSuYKi64QL8Fgc9QAAAIEA9+GghdabPd7LvKtcNrhX' \
- 'uXmUr7v6OuqC+VdMCz0HgmdRWVeOutRZT+ZxBxCBgLRJFnEj6EwoFhO3zwkyjMim4TwW' \
- 'eotUfI0o4KOuHiuzpnWRbqN/C/ohNWLx+2J6ASQ7zKTxvqhRkImog9/hWuWfBpKLZl6A' \
- 'e1UlZAFMO/7PSSoAAACBAJcQ4JODqhuGbXIEpqxetm7PWbdbCcr3y/GzIZ066pRovpL6' \
- 'qm3qCVIym4cyChxWwb8qlyCIi+YRUUWm1z/wiBYT2Vf3S4FXBnyymCkKEaV/EY7+jd4X' \
- '1bXI58OD2u+bLCB/sInM4fGB8CZUIWT9nJH0Ve9jJUge2ms348/QOJ1+'
- }.freeze
-
- MD5_FINGERPRINTS = {
- rsa: '06:b2:8a:92:df:0e:11:2c:ca:7b:8f:a4:ba:6e:4b:fd',
- ecdsa: '45:ff:5b:98:9a:b6:8a:41:13:c1:30:8b:09:5e:7b:4e',
- ed25519: '2e:65:6a:c8:cf:bf:b2:8b:9a:bd:6d:9f:11:5c:12:16',
- dss: '57:98:86:02:5f:9c:f4:9b:ad:5a:1e:51:92:0e:fd:2b'
- }.freeze
-
- BIT_COUNTS = {
- rsa: 2048,
- ecdsa: 256,
- ed25519: 256,
- dss: 1024
- }.freeze
-
- describe '#type' do
- KEYS.each do |type, key|
- it "calculates the type of #{type} keys" do
- calculated_type = described_class.new(key).type
-
- expect(calculated_type).to eq(type.to_s.upcase)
- end
- end
- end
-
- describe '#fingerprint' do
- KEYS.each do |type, key|
- it "calculates the MD5 fingerprint for #{type} keys" do
- fp = described_class.new(key).fingerprint
-
- expect(fp).to eq(MD5_FINGERPRINTS[type])
- end
- end
- end
-
- describe '#bits' do
- KEYS.each do |type, key|
- it "calculates the number of bits in #{type} keys" do
- bits = described_class.new(key).bits
-
- expect(bits).to eq(BIT_COUNTS[type])
- end
- end
- end
-
- describe '#key' do
- it 'carries the unmodified key data' do
- key = described_class.new(KEYS[:rsa]).key
-
- expect(key).to eq(KEYS[:rsa])
- end
- end
-end
diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb
index d17d440d833..d9ddb4326be 100644
--- a/spec/lib/gitlab/ldap/adapter_spec.rb
+++ b/spec/lib/gitlab/ldap/adapter_spec.rb
@@ -16,7 +16,7 @@ describe Gitlab::LDAP::Adapter do
expect(adapter).to receive(:ldap_search) do |arg|
expect(arg[:filter].to_s).to eq('(uid=johndoe)')
expect(arg[:base]).to eq('dc=example,dc=com')
- expect(arg[:attributes]).to match(%w{uid cn dn uid userid sAMAccountName mail email userPrincipalName})
+ expect(arg[:attributes]).to match(%w{dn uid cn mail email userPrincipalName})
end.and_return({})
adapter.users('uid', 'johndoe')
@@ -26,7 +26,7 @@ describe Gitlab::LDAP::Adapter do
expect(adapter).to receive(:ldap_search).with(
base: 'uid=johndoe,ou=users,dc=example,dc=com',
scope: Net::LDAP::SearchScope_BaseObject,
- attributes: %w{uid cn dn uid userid sAMAccountName mail email userPrincipalName},
+ attributes: %w{dn uid cn mail email userPrincipalName},
filter: nil
).and_return({})
@@ -63,7 +63,7 @@ describe Gitlab::LDAP::Adapter do
it 'uses the right uid attribute when non-default' do
stub_ldap_config(uid: 'sAMAccountName')
expect(adapter).to receive(:ldap_search).with(
- hash_including(attributes: %w{sAMAccountName cn dn uid userid sAMAccountName mail email userPrincipalName})
+ hash_including(attributes: %w{dn sAMAccountName cn mail email userPrincipalName})
).and_return({})
adapter.users('sAMAccountName', 'johndoe')
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb
index 57a91193004..8370adf9211 100644
--- a/spec/lib/gitlab/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb
@@ -4,8 +4,8 @@ describe Gitlab::LDAP::AuthHash do
let(:auth_hash) do
described_class.new(
OmniAuth::AuthHash.new(
- uid: '123456',
- provider: 'ldapmain',
+ uid: '123456',
+ provider: 'ldapmain',
info: info,
extra: {
raw_info: raw_info
@@ -33,11 +33,11 @@ describe Gitlab::LDAP::AuthHash do
context "without overridden attributes" do
it "has the correct username" do
- expect(auth_hash.username).to eq("123456")
+ expect(auth_hash.username).to eq("123456")
end
it "has the correct name" do
- expect(auth_hash.name).to eq("Smith, J.")
+ expect(auth_hash.name).to eq("Smith, J.")
end
end
@@ -54,11 +54,11 @@ describe Gitlab::LDAP::AuthHash do
end
it "has the correct username" do
- expect(auth_hash.username).to eq("johnsmith@example.com")
+ expect(auth_hash.username).to eq("johnsmith@example.com")
end
it "has the correct name" do
- expect(auth_hash.name).to eq("John Smith")
+ expect(auth_hash.name).to eq("John Smith")
end
end
end
diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb
index 292ec064a67..ca2213cd112 100644
--- a/spec/lib/gitlab/ldap/config_spec.rb
+++ b/spec/lib/gitlab/ldap/config_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::LDAP::Config do
describe '#initialize' do
it 'requires a provider' do
- expect{ described_class.new }.to raise_error ArgumentError
+ expect { described_class.new }.to raise_error ArgumentError
end
it 'works' do
@@ -15,7 +15,7 @@ describe Gitlab::LDAP::Config do
end
it 'raises an error if a unknown provider is used' do
- expect{ described_class.new 'unknown' }.to raise_error(RuntimeError)
+ expect { described_class.new 'unknown' }.to raise_error(RuntimeError)
end
end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 175ceec44d7..6a6e465cea2 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -37,7 +37,8 @@ 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', external_email: true, email_provider: 'ldapmain')
+ create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain')
+ ldap_user.gl_user.user_synced_attributes_metadata(provider: 'ldapmain', email: true)
expect(ldap_user.changed?).to be_falsey
end
end
@@ -61,12 +62,12 @@ describe Gitlab::LDAP::User do
it "finds the user if already existing" do
create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
- expect{ ldap_user.save }.not_to change{ User.count }
+ expect { ldap_user.save }.not_to change { User.count }
end
it "connects to existing non-ldap user if the email matches" do
existing_user = create(:omniauth_user, email: 'john@example.com', provider: "twitter")
- expect{ ldap_user.save }.not_to change{ User.count }
+ expect { ldap_user.save }.not_to change { User.count }
existing_user.reload
expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid'
@@ -75,7 +76,7 @@ describe Gitlab::LDAP::User do
it 'connects to existing ldap user if the extern_uid changes' do
existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'old-uid', provider: 'ldapmain')
- expect{ ldap_user.save }.not_to change{ User.count }
+ expect { ldap_user.save }.not_to change { User.count }
existing_user.reload
expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid'
@@ -85,7 +86,7 @@ describe Gitlab::LDAP::User do
it 'connects to existing ldap user if the extern_uid changes and email address has upper case characters' do
existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'old-uid', provider: 'ldapmain')
- expect{ ldap_user_upper_case.save }.not_to change{ User.count }
+ 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'
@@ -106,7 +107,7 @@ describe Gitlab::LDAP::User do
end
it "creates a new user if not found" do
- expect{ ldap_user.save }.to change{ User.count }.by(1)
+ expect { ldap_user.save }.to change { User.count }.by(1)
end
context 'when signup is disabled' do
@@ -141,12 +142,12 @@ describe Gitlab::LDAP::User do
expect(ldap_user.gl_user.email).to eq(info[:email])
end
- it "has external_email set to true" do
- expect(ldap_user.gl_user.external_email?).to be(true)
+ it "has user_synced_attributes_metadata email set to true" do
+ expect(ldap_user.gl_user.user_synced_attributes_metadata.email_synced).to be_truthy
end
- it "has email_provider set to provider" do
- expect(ldap_user.gl_user.email_provider).to eql 'ldapmain'
+ it "has synced_attribute_provider set to ldapmain" do
+ expect(ldap_user.gl_user.user_synced_attributes_metadata.provider).to eql 'ldapmain'
end
end
@@ -156,11 +157,11 @@ describe Gitlab::LDAP::User do
end
it "has a temp email" do
- expect(ldap_user.gl_user.temp_oauth_email?).to be(true)
+ expect(ldap_user.gl_user.temp_oauth_email?).to be_truthy
end
- it "has external_email set to false" do
- expect(ldap_user.gl_user.external_email?).to be(false)
+ it "has synced attribute email set to false" do
+ expect(ldap_user.gl_user.user_synced_attributes_metadata.email_synced).to be_falsey
end
end
end
@@ -168,7 +169,7 @@ describe Gitlab::LDAP::User do
describe 'blocking' do
def configure_block(value)
allow_any_instance_of(Gitlab::LDAP::Config)
- .to receive(:block_auto_created_users).and_return(value)
+ .to receive(:block_auto_created_users).and_return(value)
end
context 'signup' do
diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
index 461b1e4182a..ebe66948a91 100644
--- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb
@@ -4,10 +4,6 @@ describe Gitlab::Metrics::RequestsRackMiddleware do
let(:app) { double('app') }
subject { described_class.new(app) }
- around do |example|
- Timecop.freeze { example.run }
- end
-
describe '#call' do
let(:status) { 100 }
let(:env) { { 'REQUEST_METHOD' => 'GET' } }
@@ -28,16 +24,14 @@ describe Gitlab::Metrics::RequestsRackMiddleware do
subject.call(env)
end
- it 'measures execution time' do
- execution_time = 10
- allow(app).to receive(:call) do |*args|
- Timecop.freeze(execution_time.seconds)
- [200, nil, nil]
- end
+ RSpec::Matchers.define :a_positive_execution_time do
+ match { |actual| actual > 0 }
+ end
- expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ status: 200, method: 'get' }, execution_time)
+ it 'measures execution time' do
+ expect(described_class).to receive_message_chain(:http_request_duration_seconds, :observe).with({ status: 200, method: 'get' }, a_positive_execution_time)
- subject.call(env)
+ Timecop.scale(3600) { subject.call(env) }
end
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 6c84c4a0c1e..8aaf320cbf5 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -10,7 +10,11 @@ describe Gitlab::OAuth::User do
{
nickname: '-john+gitlab-ETC%.git@gmail.com',
name: 'John',
- email: 'john@mail.com'
+ email: 'john@mail.com',
+ address: {
+ locality: 'locality',
+ country: 'country'
+ }
}
end
let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') }
@@ -147,7 +151,7 @@ describe Gitlab::OAuth::User do
end
it 'throws an error' do
- expect{ oauth_user.save }.to raise_error StandardError
+ expect { oauth_user.save }.to raise_error StandardError
end
end
@@ -157,7 +161,7 @@ describe Gitlab::OAuth::User do
end
it 'throws an error' do
- expect{ oauth_user.save }.to raise_error StandardError
+ expect { oauth_user.save }.to raise_error StandardError
end
end
end
@@ -422,11 +426,12 @@ describe Gitlab::OAuth::User do
end
end
- describe 'updating email' do
+ describe 'ensure backwards compatibility with with sync email from provider option' do
let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
before do
stub_omniauth_config(sync_email_from_provider: 'my-provider')
+ stub_omniauth_config(sync_profile_from_provider: ['my-provider'])
end
context "when provider sets an email" do
@@ -434,12 +439,12 @@ describe Gitlab::OAuth::User do
expect(gl_user.email).to eq(info_hash[:email])
end
- it "has external_email set to true" do
- expect(gl_user.external_email?).to be(true)
+ it "has external_attributes set to true" do
+ expect(gl_user.user_synced_attributes_metadata).not_to be_nil
end
- it "has email_provider set to provider" do
- expect(gl_user.email_provider).to eql 'my-provider'
+ it "has attributes_provider set to my-provider" do
+ expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider'
end
end
@@ -452,8 +457,9 @@ describe Gitlab::OAuth::User do
expect(gl_user.email).not_to eq(info_hash[:email])
end
- it "has external_email set to false" do
- expect(gl_user.external_email?).to be(false)
+ it "has user_synced_attributes_metadata set to nil" do
+ expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider'
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be_falsey
end
end
end
@@ -481,10 +487,178 @@ describe Gitlab::OAuth::User do
email: 'admin@othermail.com'
}
end
-
+
it 'generates the username with a counter' do
expect(gl_user.username).to eq('admin1')
end
end
end
+
+ describe 'updating email with sync profile' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ before do
+ stub_omniauth_config(sync_profile_from_provider: ['my-provider'])
+ stub_omniauth_config(sync_profile_attributes: true)
+ end
+
+ context "when provider sets an email" do
+ it "updates the user email" do
+ expect(gl_user.email).to eq(info_hash[:email])
+ end
+
+ it "has email_synced_attribute set to true" do
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true)
+ end
+
+ it "has my-provider as attributes_provider" do
+ expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider'
+ end
+ end
+
+ context "when provider doesn't set an email" do
+ before do
+ info_hash.delete(:email)
+ end
+
+ it "does not update the user email" do
+ expect(gl_user.email).not_to eq(info_hash[:email])
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be(false)
+ end
+ end
+ end
+
+ describe 'updating name' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ before do
+ stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
+ stub_omniauth_setting(sync_profile_attributes: true)
+ end
+
+ context "when provider sets a name" do
+ it "updates the user name" do
+ expect(gl_user.name).to eq(info_hash[:name])
+ end
+ end
+
+ context "when provider doesn't set a name" do
+ before do
+ info_hash.delete(:name)
+ end
+
+ it "does not update the user name" do
+ expect(gl_user.name).not_to eq(info_hash[:name])
+ expect(gl_user.user_synced_attributes_metadata.name_synced).to be(false)
+ end
+ end
+ end
+
+ describe 'updating location' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ before do
+ stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
+ stub_omniauth_setting(sync_profile_attributes: true)
+ end
+
+ context "when provider sets a location" do
+ it "updates the user location" do
+ expect(gl_user.location).to eq(info_hash[:address][:locality] + ', ' + info_hash[:address][:country])
+ expect(gl_user.user_synced_attributes_metadata.location_synced).to be(true)
+ end
+ end
+
+ context "when provider doesn't set a location" do
+ before do
+ info_hash[:address].delete(:country)
+ info_hash[:address].delete(:locality)
+ end
+
+ it "does not update the user location" do
+ expect(gl_user.location).to be_nil
+ expect(gl_user.user_synced_attributes_metadata.location_synced).to be(false)
+ end
+ end
+ end
+
+ describe 'updating user info' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ context "update all info" do
+ before do
+ stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
+ stub_omniauth_setting(sync_profile_attributes: true)
+ end
+
+ it "updates the user email" do
+ expect(gl_user.email).to eq(info_hash[:email])
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true)
+ end
+
+ it "updates the user name" do
+ expect(gl_user.name).to eq(info_hash[:name])
+ expect(gl_user.user_synced_attributes_metadata.name_synced).to be(true)
+ end
+
+ it "updates the user location" do
+ expect(gl_user.location).to eq(info_hash[:address][:locality] + ', ' + info_hash[:address][:country])
+ expect(gl_user.user_synced_attributes_metadata.location_synced).to be(true)
+ end
+
+ it "sets my-provider as the attributes provider" do
+ expect(gl_user.user_synced_attributes_metadata.provider).to eql('my-provider')
+ end
+ end
+
+ context "update only requested info" do
+ before do
+ stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
+ stub_omniauth_setting(sync_profile_attributes: %w(name location))
+ end
+
+ it "updates the user name" do
+ expect(gl_user.name).to eq(info_hash[:name])
+ expect(gl_user.user_synced_attributes_metadata.name_synced).to be(true)
+ end
+
+ it "updates the user location" do
+ expect(gl_user.location).to eq(info_hash[:address][:locality] + ', ' + info_hash[:address][:country])
+ expect(gl_user.user_synced_attributes_metadata.location_synced).to be(true)
+ end
+
+ it "does not update the user email" do
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be(false)
+ end
+ end
+
+ context "update default_scope" do
+ before do
+ stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
+ end
+
+ it "updates the user email" do
+ expect(gl_user.email).to eq(info_hash[:email])
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true)
+ end
+ end
+
+ context "update no info when profile sync is nil" do
+ it "does not have sync_attribute" do
+ expect(gl_user.user_synced_attributes_metadata).to be(nil)
+ end
+
+ it "does not update the user email" do
+ expect(gl_user.email).not_to eq(info_hash[:email])
+ end
+
+ it "does not update the user name" do
+ expect(gl_user.name).not_to eq(info_hash[:name])
+ end
+
+ it "does not update the user location" do
+ expect(gl_user.location).not_to eq(info_hash[:address][:country])
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
index 12e75cdd5d0..d19bd611919 100644
--- a/spec/lib/gitlab/project_template_spec.rb
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -4,7 +4,9 @@ describe Gitlab::ProjectTemplate do
describe '.all' do
it 'returns a all templates' do
expected = [
- described_class.new('rails', 'Ruby on Rails')
+ described_class.new('rails', 'Ruby on Rails'),
+ described_class.new('spring', 'Spring'),
+ described_class.new('express', 'NodeJS Express')
]
expect(described_class.all).to be_an(Array)
diff --git a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb
index d7df4e35c31..5589db92b1d 100644
--- a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb
+++ b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::Prometheus::AdditionalMetricsParser do
queries: [{ query_range: 'query_range_empty' }]
- group: group_b
priority: 1
- metrics:
+ metrics:
- title: title
required_metrics: ['metric_a']
weight: 1
@@ -148,7 +148,7 @@ describe Gitlab::Prometheus::AdditionalMetricsParser do
- group: group_a
priority: 1
metrics:
- - title:
+ - title:
required_metrics: []
weight: 1
queries: []
diff --git a/spec/lib/gitlab/redis/wrapper_spec.rb b/spec/lib/gitlab/redis/wrapper_spec.rb
index e1becd0a614..0c22a0d62cc 100644
--- a/spec/lib/gitlab/redis/wrapper_spec.rb
+++ b/spec/lib/gitlab/redis/wrapper_spec.rb
@@ -17,4 +17,11 @@ describe Gitlab::Redis::Wrapper do
let(:class_redis_url) { Gitlab::Redis::Wrapper::DEFAULT_REDIS_URL }
include_examples "redis_shared_examples"
+
+ describe '.config_file_path' do
+ it 'returns the absolute path to the configuration file' do
+ expect(described_class.config_file_path('foo.yml'))
+ .to eq Rails.root.join('config', 'foo.yml').to_s
+ end
+ end
end
diff --git a/spec/lib/gitlab/reference_counter_spec.rb b/spec/lib/gitlab/reference_counter_spec.rb
new file mode 100644
index 00000000000..b2344d1870a
--- /dev/null
+++ b/spec/lib/gitlab/reference_counter_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::ReferenceCounter do
+ let(:redis) { double('redis') }
+ let(:reference_counter_key) { "git-receive-pack-reference-counter:project-1" }
+ let(:reference_counter) { described_class.new('project-1') }
+
+ before do
+ allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
+ end
+
+ it 'increases and set the expire time of a reference count for a path' do
+ expect(redis).to receive(:incr).with(reference_counter_key)
+ expect(redis).to receive(:expire).with(reference_counter_key,
+ described_class::REFERENCE_EXPIRE_TIME)
+ expect(reference_counter.increase).to be(true)
+ end
+
+ it 'decreases the reference count for a path' do
+ allow(redis).to receive(:decr).and_return(0)
+ expect(redis).to receive(:decr).with(reference_counter_key)
+ expect(reference_counter.decrease).to be(true)
+ end
+
+ it 'warns if attempting to decrease a counter with a value of one or less, and resets the counter' do
+ expect(redis).to receive(:decr).and_return(-1)
+ expect(redis).to receive(:del)
+ expect(Rails.logger).to receive(:warn).with("Reference counter for project-1" \
+ " decreased when its value was less than 1. Reseting the counter.")
+ expect(reference_counter.decrease).to be(true)
+ end
+
+ it 'get the reference count for a path' do
+ allow(redis).to receive(:get).and_return(1)
+ expect(reference_counter.value).to be(1)
+ end
+end
diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb
index e272bdb9284..8a28ad0e597 100644
--- a/spec/lib/gitlab/request_context_spec.rb
+++ b/spec/lib/gitlab/request_context_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::RequestContext do
let(:env) { Hash.new }
context 'when RequestStore::Middleware is used' do
- around(:each) do |example|
+ around do |example|
RequestStore::Middleware.new(-> (env) { example.run }).call({})
end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 2827a18515e..19710029224 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -109,7 +109,7 @@ describe Gitlab::Saml::User do
end
it 'does not throw an error' do
- expect{ saml_user.save }.not_to raise_error
+ expect { saml_user.save }.not_to raise_error
end
end
@@ -119,7 +119,7 @@ describe Gitlab::Saml::User do
end
it 'throws an error' do
- expect{ saml_user.save }.to raise_error StandardError
+ expect { saml_user.save }.to raise_error StandardError
end
end
end
diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb
new file mode 100644
index 00000000000..8c211d1c63f
--- /dev/null
+++ b/spec/lib/gitlab/sentry_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe Gitlab::Sentry do
+ describe '.context' do
+ it 'adds the locale to the tags' do
+ expect(described_class).to receive(:enabled?).and_return(true)
+
+ described_class.context(nil)
+
+ expect(Raven.tags_context[:locale]).to eq(I18n.locale.to_s)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 2345874cf10..c7930378240 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -94,28 +94,41 @@ describe Gitlab::Shell do
end
describe 'projects commands' do
- let(:projects_path) { 'tmp/tests/shell-projects-test/bin/gitlab-projects' }
+ 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('tmp/tests/shell-projects-test')
+ 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 '#add_repository' do
- it 'returns true when the command succeeds' do
- expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'add-project', 'current/storage', 'project/path.git'],
- nil, popen_vars).and_return([nil, 0])
+ 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')
+
+ repo_stat = File.stat(created_path) rescue nil
+ hooks_stat = File.lstat(hooks_path) rescue nil
+ hooks_dir = File.realpath(hooks_path)
+ ensure
+ FileUtils.rm_rf(created_path)
+ end
- expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be true
+ 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)
end
it 'returns false when the command fails' do
- expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'add-project', 'current/storage', 'project/path.git'],
- nil, popen_vars).and_return(["error", 1])
+ expect(FileUtils).to receive(:mkdir_p).and_raise(Errno::EEXIST)
- expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be false
+ expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be_falsy
end
end
@@ -173,22 +186,48 @@ describe Gitlab::Shell do
end
end
- describe '#fetch_remote' do
+ shared_examples 'fetch_remote' do |gitaly_on|
+ let(:project2) { create(:project, :repository) }
+ let(:repository) { project2.repository }
+
def fetch_remote(ssh_auth = nil)
- gitlab_shell.fetch_remote('current/storage', 'project/path', 'new/storage', ssh_auth: ssh_auth)
+ gitlab_shell.fetch_remote(repository.raw_repository, 'new/storage', ssh_auth: ssh_auth)
end
- def expect_popen(vars = {})
+ def expect_popen(fail = false, vars = {})
popen_args = [
projects_path,
'fetch-remote',
- 'current/storage',
- 'project/path.git',
+ TestEnv.repos_path,
+ repository.relative_path,
'new/storage',
Gitlab.config.gitlab_shell.git_timeout.to_s
]
- expect(Gitlab::Popen).to receive(:popen).with(popen_args, nil, popen_vars.merge(vars))
+ return_value = fail ? ["error", 1] : [nil, 0]
+
+ expect(Gitlab::Popen).to receive(:popen).with(popen_args, nil, popen_vars.merge(vars)).and_return(return_value)
+ end
+
+ def expect_gitaly_call(fail, vars = {})
+ receive_fetch_remote =
+ if fail
+ receive(:fetch_remote).and_raise(GRPC::NotFound)
+ else
+ receive(:fetch_remote).and_return(true)
+ end
+
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive_fetch_remote
+ end
+
+ if gitaly_on
+ def expect_call(fail, vars = {})
+ expect_gitaly_call(fail, vars)
+ end
+ else
+ def expect_call(fail, vars = {})
+ expect_popen(fail, vars)
+ end
end
def build_ssh_auth(opts = {})
@@ -203,20 +242,20 @@ describe Gitlab::Shell do
end
it 'returns true when the command succeeds' do
- expect_popen.and_return([nil, 0])
+ expect_call(false)
expect(fetch_remote).to be_truthy
end
it 'raises an exception when the command fails' do
- expect_popen.and_return(["error", 1])
+ expect_call(true)
- expect { fetch_remote }.to raise_error(Gitlab::Shell::Error, "error")
+ expect { fetch_remote }.to raise_error(Gitlab::Shell::Error)
end
context 'SSH auth' do
it 'passes the SSH key if specified' do
- expect_popen('GITLAB_SHELL_SSH_KEY' => 'foo').and_return([nil, 0])
+ expect_call(false, 'GITLAB_SHELL_SSH_KEY' => 'foo')
ssh_auth = build_ssh_auth(ssh_key_auth?: true, ssh_private_key: 'foo')
@@ -224,7 +263,7 @@ describe Gitlab::Shell do
end
it 'does not pass an empty SSH key' do
- expect_popen.and_return([nil, 0])
+ expect_call(false)
ssh_auth = build_ssh_auth(ssh_key_auth: true, ssh_private_key: '')
@@ -232,7 +271,7 @@ describe Gitlab::Shell do
end
it 'does not pass the key unless SSH key auth is to be used' do
- expect_popen.and_return([nil, 0])
+ expect_call(false)
ssh_auth = build_ssh_auth(ssh_key_auth: false, ssh_private_key: 'foo')
@@ -240,7 +279,7 @@ describe Gitlab::Shell do
end
it 'passes the known_hosts data if specified' do
- expect_popen('GITLAB_SHELL_KNOWN_HOSTS' => 'foo').and_return([nil, 0])
+ expect_call(false, 'GITLAB_SHELL_KNOWN_HOSTS' => 'foo')
ssh_auth = build_ssh_auth(ssh_known_hosts: 'foo')
@@ -248,7 +287,7 @@ describe Gitlab::Shell do
end
it 'does not pass empty known_hosts data' do
- expect_popen.and_return([nil, 0])
+ expect_call(false)
ssh_auth = build_ssh_auth(ssh_known_hosts: '')
@@ -256,7 +295,7 @@ describe Gitlab::Shell do
end
it 'does not pass known_hosts data unless SSH is to be used' do
- expect_popen(popen_vars).and_return([nil, 0])
+ expect_call(false, popen_vars)
ssh_auth = build_ssh_auth(ssh_import?: false, ssh_known_hosts: 'foo')
@@ -265,6 +304,14 @@ describe Gitlab::Shell do
end
end
+ describe '#fetch_remote local', skip_gitaly_mock: true do
+ it_should_behave_like 'fetch_remote', false
+ end
+
+ describe '#fetch_remote gitaly' do
+ it_should_behave_like 'fetch_remote', true
+ end
+
describe '#import_repository' do
it 'returns true when the command succeeds' do
expect(Gitlab::Popen).to receive(:popen)
diff --git a/spec/lib/gitlab/sidekiq_throttler_spec.rb b/spec/lib/gitlab/sidekiq_throttler_spec.rb
index 6374ac80207..2dbb7bb7c34 100644
--- a/spec/lib/gitlab/sidekiq_throttler_spec.rb
+++ b/spec/lib/gitlab/sidekiq_throttler_spec.rb
@@ -1,28 +1,44 @@
require 'spec_helper'
describe Gitlab::SidekiqThrottler do
- before do
- Sidekiq.options[:concurrency] = 35
-
- stub_application_setting(
- sidekiq_throttling_enabled: true,
- sidekiq_throttling_factor: 0.1,
- sidekiq_throttling_queues: %w[build project_cache]
- )
- end
-
describe '#execute!' do
- it 'sets limits on the selected queues' do
- described_class.execute!
+ context 'when job throttling is enabled' do
+ before do
+ Sidekiq.options[:concurrency] = 35
+
+ stub_application_setting(
+ sidekiq_throttling_enabled: true,
+ sidekiq_throttling_factor: 0.1,
+ sidekiq_throttling_queues: %w[build project_cache]
+ )
+ end
+
+ it 'requires sidekiq-limit_fetch' do
+ expect(described_class).to receive(:require).with('sidekiq-limit_fetch').and_call_original
+
+ described_class.execute!
+ end
+
+ it 'sets limits on the selected queues' do
+ described_class.execute!
+
+ expect(Sidekiq::Queue['build'].limit).to eq 4
+ expect(Sidekiq::Queue['project_cache'].limit).to eq 4
+ end
+
+ it 'does not set limits on other queues' do
+ described_class.execute!
- expect(Sidekiq::Queue['build'].limit).to eq 4
- expect(Sidekiq::Queue['project_cache'].limit).to eq 4
+ expect(Sidekiq::Queue['merge'].limit).to be_nil
+ end
end
- it 'does not set limits on other queues' do
- described_class.execute!
+ context 'when job throttling is disabled' do
+ it 'does not require sidekiq-limit_fetch' do
+ expect(described_class).not_to receive(:require).with('sidekiq-limit_fetch')
- expect(Sidekiq::Queue['merge'].limit).to be_nil
+ described_class.execute!
+ end
end
end
end
diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb
new file mode 100644
index 00000000000..48d56628ed5
--- /dev/null
+++ b/spec/lib/gitlab/sql/pattern_spec.rb
@@ -0,0 +1,175 @@
+require 'spec_helper'
+
+describe Gitlab::SQL::Pattern do
+ describe '.to_pattern' do
+ subject(:to_pattern) { User.to_pattern(query) }
+
+ context 'when a query is shorter than 3 chars' do
+ let(:query) { '12' }
+
+ it 'returns exact matching pattern' do
+ expect(to_pattern).to eq('12')
+ end
+ end
+
+ context 'when a query with a escape character is shorter than 3 chars' do
+ let(:query) { '_2' }
+
+ it 'returns sanitized exact matching pattern' do
+ expect(to_pattern).to eq('\_2')
+ end
+ end
+
+ context 'when a query is equal to 3 chars' do
+ let(:query) { '123' }
+
+ it 'returns partial matching pattern' do
+ expect(to_pattern).to eq('%123%')
+ end
+ end
+
+ context 'when a query with a escape character is equal to 3 chars' do
+ let(:query) { '_23' }
+
+ it 'returns partial matching pattern' do
+ expect(to_pattern).to eq('%\_23%')
+ end
+ end
+
+ context 'when a query is longer than 3 chars' do
+ let(:query) { '1234' }
+
+ it 'returns partial matching pattern' do
+ expect(to_pattern).to eq('%1234%')
+ end
+ end
+
+ context 'when a query with a escape character is longer than 3 chars' do
+ let(:query) { '_234' }
+
+ it 'returns sanitized partial matching pattern' do
+ expect(to_pattern).to eq('%\_234%')
+ end
+ end
+ end
+
+ describe '.select_fuzzy_words' do
+ subject(:select_fuzzy_words) { Issue.select_fuzzy_words(query) }
+
+ context 'with a word equal to 3 chars' do
+ let(:query) { 'foo' }
+
+ it 'returns array cotaining a word' do
+ expect(select_fuzzy_words).to match_array(['foo'])
+ end
+ end
+
+ context 'with a word shorter than 3 chars' do
+ let(:query) { 'fo' }
+
+ it 'returns empty array' do
+ expect(select_fuzzy_words).to match_array([])
+ end
+ end
+
+ context 'with two words both equal to 3 chars' do
+ let(:query) { 'foo baz' }
+
+ it 'returns array containing two words' do
+ expect(select_fuzzy_words).to match_array(%w[foo baz])
+ end
+ end
+
+ context 'with two words divided by two spaces both equal to 3 chars' do
+ let(:query) { 'foo baz' }
+
+ it 'returns array containing two words' do
+ expect(select_fuzzy_words).to match_array(%w[foo baz])
+ end
+ end
+
+ context 'with two words equal to 3 chars and shorter than 3 chars' do
+ let(:query) { 'foo ba' }
+
+ it 'returns array containing a word' do
+ expect(select_fuzzy_words).to match_array(['foo'])
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote' do
+ let(:query) { '"really bar"' }
+
+ it 'returns array containing a multi-word' do
+ expect(select_fuzzy_words).to match_array(['really bar'])
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote and two words' do
+ let(:query) { 'foo "really bar" baz' }
+
+ it 'returns array containing a multi-word and tow words' do
+ expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz'])
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote missing a spece before the first double quote' do
+ let(:query) { 'foo"really bar"' }
+
+ it 'returns array containing two words with double quote' do
+ expect(select_fuzzy_words).to match_array(['foo"really', 'bar"'])
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote missing a spece after the second double quote' do
+ let(:query) { '"really bar"baz' }
+
+ it 'returns array containing two words with double quote' do
+ expect(select_fuzzy_words).to match_array(['"really', 'bar"baz'])
+ end
+ end
+
+ context 'with two multi-word surrounded by double quote and two words' do
+ let(:query) { 'foo "really bar" baz "awesome feature"' }
+
+ it 'returns array containing two multi-words and tow words' do
+ expect(select_fuzzy_words).to match_array(['foo', 'really bar', 'baz', 'awesome feature'])
+ end
+ end
+ end
+
+ describe '.to_fuzzy_arel' do
+ subject(:to_fuzzy_arel) { Issue.to_fuzzy_arel(:title, query) }
+
+ context 'with a word equal to 3 chars' do
+ let(:query) { 'foo' }
+
+ it 'returns a single ILIKE condition' do
+ expect(to_fuzzy_arel.to_sql).to match(/title.*I?LIKE '\%foo\%'/)
+ end
+ end
+
+ context 'with a word shorter than 3 chars' do
+ let(:query) { 'fo' }
+
+ it 'returns nil' do
+ expect(to_fuzzy_arel).to be_nil
+ end
+ end
+
+ context 'with two words both equal to 3 chars' do
+ let(:query) { 'foo baz' }
+
+ it 'returns a joining LIKE condition using a AND' do
+ expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%'/)
+ end
+ end
+
+ context 'with a multi-word surrounded by double quote and two words' do
+ let(:query) { 'foo "really bar" baz' }
+
+ it 'returns a joining LIKE condition using a AND' do
+ expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%' AND .*title.*I?LIKE '\%really bar\%'/)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb
index 5346881444d..baf8f6644bf 100644
--- a/spec/lib/gitlab/sql/union_spec.rb
+++ b/spec/lib/gitlab/sql/union_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::SQL::Union do
empty_relation = User.none
union = described_class.new([empty_relation, relation_1, relation_2])
- expect{User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error
+ 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
end
diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb
new file mode 100644
index 00000000000..93d538141ce
--- /dev/null
+++ b/spec/lib/gitlab/ssh_public_key_spec.rb
@@ -0,0 +1,136 @@
+require 'spec_helper'
+
+describe Gitlab::SSHPublicKey, lib: true do
+ let(:key) { attributes_for(:rsa_key_2048)[:key] }
+ let(:public_key) { described_class.new(key) }
+
+ describe '.technology(name)' do
+ it 'returns nil for an unrecognised name' do
+ expect(described_class.technology(:foo)).to be_nil
+ end
+
+ where(:name) do
+ [:rsa, :dsa, :ecdsa, :ed25519]
+ end
+
+ with_them do
+ it { expect(described_class.technology(name).name).to eq(name) }
+ it { expect(described_class.technology(name.to_s).name).to eq(name) }
+ end
+ end
+
+ describe '.supported_sizes(name)' do
+ where(:name, :sizes) do
+ [
+ [:rsa, [1024, 2048, 3072, 4096]],
+ [:dsa, [1024, 2048, 3072]],
+ [:ecdsa, [256, 384, 521]],
+ [:ed25519, [256]]
+ ]
+ end
+
+ subject { described_class.supported_sizes(name) }
+
+ with_them do
+ it { expect(described_class.supported_sizes(name)).to eq(sizes) }
+ it { expect(described_class.supported_sizes(name.to_s)).to eq(sizes) }
+ end
+ end
+
+ describe '#valid?' do
+ subject { public_key }
+
+ context 'with a valid SSH key' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'with an invalid SSH key' do
+ let(:key) { 'this is not a key' }
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
+ describe '#type' do
+ subject { public_key.type }
+
+ where(:factory, :type) do
+ [
+ [:rsa_key_2048, :rsa],
+ [:dsa_key_2048, :dsa],
+ [:ecdsa_key_256, :ecdsa],
+ [:ed25519_key_256, :ed25519]
+ ]
+ end
+
+ with_them do
+ let(:key) { attributes_for(factory)[:key] }
+
+ it { is_expected.to eq(type) }
+ end
+
+ context 'with an invalid SSH key' do
+ let(:key) { 'this is not a key' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#bits' do
+ subject { public_key.bits }
+
+ where(:factory, :bits) do
+ [
+ [:rsa_key_2048, 2048],
+ [:dsa_key_2048, 2048],
+ [:ecdsa_key_256, 256],
+ [:ed25519_key_256, 256]
+ ]
+ end
+
+ with_them do
+ let(:key) { attributes_for(factory)[:key] }
+
+ it { is_expected.to eq(bits) }
+ end
+
+ context 'with an invalid SSH key' do
+ let(:key) { 'this is not a key' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#fingerprint' do
+ subject { public_key.fingerprint }
+
+ where(:factory, :fingerprint) do
+ [
+ [:rsa_key_2048, '2e:ca:dc:e0:37:29:ed:fc:f0:1d:bf:66:d4:cd:51:b1'],
+ [:dsa_key_2048, 'bc:c1:a4:be:7e:8c:84:56:b3:58:93:53:c6:80:78:8c'],
+ [:ecdsa_key_256, '67:a3:a9:7d:b8:e1:15:d4:80:40:21:34:bb:ed:97:38'],
+ [:ed25519_key_256, 'e6:eb:45:8a:3c:59:35:5f:e9:5b:80:12:be:7e:22:73']
+ ]
+ end
+
+ with_them do
+ let(:key) { attributes_for(factory)[:key] }
+
+ it { is_expected.to eq(fingerprint) }
+ end
+
+ context 'with an invalid SSH key' do
+ let(:key) { 'this is not a key' }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#key_text' do
+ let(:key) { 'this is not a key' }
+
+ it 'carries the unmodified key data' do
+ expect(public_key.key_text).to eq(key)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/string_range_marker_spec.rb b/spec/lib/gitlab/string_range_marker_spec.rb
index abeaa7f0ddb..6bc02459dbd 100644
--- a/spec/lib/gitlab/string_range_marker_spec.rb
+++ b/spec/lib/gitlab/string_range_marker_spec.rb
@@ -2,34 +2,39 @@ require 'spec_helper'
describe Gitlab::StringRangeMarker do
describe '#mark' do
+ def mark_diff(rich = nil)
+ raw = 'abc <def>'
+ inline_diffs = [2..5]
+
+ described_class.new(raw, rich).mark(inline_diffs) do |text, left:, right:|
+ "LEFT#{text}RIGHT"
+ end
+ end
+
context "when the rich text is html safe" do
- let(:raw) { "abc <def>" }
let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">&lt;def&gt;</span>}.html_safe }
- let(:inline_diffs) { [2..5] }
- subject do
- described_class.new(raw, rich).mark(inline_diffs) do |text, left:, right:|
- "LEFT#{text}RIGHT"
- end
- end
it 'marks the inline diffs' do
- expect(subject).to eq(%{<span class="abc">abLEFTcRIGHT</span><span class="space">LEFT RIGHT</span><span class="def">LEFT&lt;dRIGHTef&gt;</span>})
- expect(subject).to be_html_safe
+ expect(mark_diff(rich)).to eq(%{<span class="abc">abLEFTcRIGHT</span><span class="space">LEFT RIGHT</span><span class="def">LEFT&lt;dRIGHTef&gt;</span>})
+ expect(mark_diff(rich)).to be_html_safe
end
end
context "when the rich text is not html safe" do
- let(:raw) { "abc <def>" }
- let(:inline_diffs) { [2..5] }
- subject do
- described_class.new(raw).mark(inline_diffs) do |text, left:, right:|
- "LEFT#{text}RIGHT"
+ context 'when rich text equals raw text' do
+ it 'marks the inline diffs' do
+ expect(mark_diff).to eq(%{abLEFTc <dRIGHTef>})
+ expect(mark_diff).not_to be_html_safe
end
end
- it 'marks the inline diffs' do
- expect(subject).to eq(%{abLEFTc &lt;dRIGHTef&gt;})
- expect(subject).to be_html_safe
+ context 'when rich text doeas not equal raw text' do
+ let(:rich) { "abc <def> differs" }
+
+ it 'marks the inline diffs' do
+ expect(mark_diff(rich)).to eq(%{abLEFTc &lt;dRIGHTef&gt; differs})
+ expect(mark_diff(rich)).to be_html_safe
+ end
end
end
end
diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
index 6541326d1de..e2fa76522bc 100644
--- a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
+++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
@@ -30,6 +30,14 @@ describe Gitlab::Template::GitlabCiYmlTemplate do
end
end
+ describe '.by_category' do
+ it 'returns sorted results' do
+ result = described_class.by_category('General')
+
+ expect(result).to eq(result.sort)
+ end
+ end
+
describe '#content' do
it 'loads the full file' do
gitignore = subject.new(Rails.root.join('vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml'))
@@ -38,4 +46,14 @@ describe Gitlab::Template::GitlabCiYmlTemplate do
expect(gitignore.content).to start_with('#')
end
end
+
+ describe '#<=>' do
+ it 'sorts lexicographically' do
+ one = described_class.new('a.gitlab-ci.yml')
+ other = described_class.new('z.gitlab-ci.yml')
+
+ expect(one.<=>(other)).to be(-1)
+ expect([other, one].sort).to eq([one, other])
+ end
+ end
end
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
index 6e0b1075a89..7098499f996 100644
--- a/spec/lib/gitlab/template/issue_template_spec.rb
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -1,41 +1,28 @@
require 'spec_helper'
describe Gitlab::Template::IssueTemplate do
- subject { described_class }
-
- let(:user) { create(:user) }
-
- let(:project) do
- create(:project,
- :repository,
- create_template: {
- user: user,
- access: Gitlab::Access::MASTER,
- path: 'issue_templates'
- })
- end
+ let(:project) { create(:project, :repository, create_templates: :issue) }
describe '.all' do
it 'strips the md suffix' do
- expect(subject.all(project).first.name).not_to end_with('.issue_template')
+ expect(described_class.all(project).first.name).not_to end_with('.issue_template')
end
it 'combines the globals and rest' do
- all = subject.all(project).map(&:name)
+ all = described_class.all(project).map(&:name)
expect(all).to include('bug')
expect(all).to include('feature_proposal')
- expect(all).to include('template_test')
end
end
describe '.find' do
it 'returns nil if the file does not exist' do
- expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ expect { described_class.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
end
it 'returns the issue object of a valid file' do
- ruby = subject.find('bug', project)
+ ruby = described_class.find('bug', project)
expect(ruby).to be_a described_class
expect(ruby.name).to eq('bug')
@@ -44,21 +31,17 @@ describe Gitlab::Template::IssueTemplate do
describe '.by_category' do
it 'return array of templates' do
- all = subject.by_category('', project).map(&:name)
+ all = described_class.by_category('', project).map(&:name)
expect(all).to include('bug')
expect(all).to include('feature_proposal')
- expect(all).to include('template_test')
end
context 'when repo is bare or empty' do
let(:empty_project) { create(:project) }
- before do
- empty_project.add_user(user, Gitlab::Access::MASTER)
- end
-
it "returns empty array" do
- templates = subject.by_category('', empty_project)
+ templates = described_class.by_category('', empty_project)
+
expect(templates).to be_empty
end
end
@@ -66,26 +49,23 @@ describe Gitlab::Template::IssueTemplate do
describe '#content' do
it 'loads the full file' do
- issue_template = subject.new('.gitlab/issue_templates/bug.md', project)
+ issue_template = described_class.new('.gitlab/issue_templates/bug.md', project)
expect(issue_template.name).to eq 'bug'
expect(issue_template.content).to eq('something valid')
end
it 'raises error when file is not found' do
- issue_template = subject.new('.gitlab/issue_templates/bugnot.md', project)
+ issue_template = described_class.new('.gitlab/issue_templates/bugnot.md', project)
expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
end
context "when repo is empty" do
let(:empty_project) { create(:project) }
- before do
- empty_project.add_user(user, Gitlab::Access::MASTER)
- end
-
it "raises file not found" do
- issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project)
+ issue_template = described_class.new('.gitlab/issue_templates/not_existent.md', empty_project)
+
expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
end
end
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
index b952274cd24..bd7ff64aa8a 100644
--- a/spec/lib/gitlab/template/merge_request_template_spec.rb
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -1,41 +1,28 @@
require 'spec_helper'
describe Gitlab::Template::MergeRequestTemplate do
- subject { described_class }
-
- let(:user) { create(:user) }
-
- let(:project) do
- create(:project,
- :repository,
- create_template: {
- user: user,
- access: Gitlab::Access::MASTER,
- path: 'merge_request_templates'
- })
- end
+ let(:project) { create(:project, :repository, create_templates: :merge_request) }
describe '.all' do
it 'strips the md suffix' do
- expect(subject.all(project).first.name).not_to end_with('.issue_template')
+ expect(described_class.all(project).first.name).not_to end_with('.issue_template')
end
it 'combines the globals and rest' do
- all = subject.all(project).map(&:name)
+ all = described_class.all(project).map(&:name)
expect(all).to include('bug')
expect(all).to include('feature_proposal')
- expect(all).to include('template_test')
end
end
describe '.find' do
it 'returns nil if the file does not exist' do
- expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ expect { described_class.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
end
it 'returns the merge request object of a valid file' do
- ruby = subject.find('bug', project)
+ ruby = described_class.find('bug', project)
expect(ruby).to be_a described_class
expect(ruby.name).to eq('bug')
@@ -44,21 +31,17 @@ describe Gitlab::Template::MergeRequestTemplate do
describe '.by_category' do
it 'return array of templates' do
- all = subject.by_category('', project).map(&:name)
+ all = described_class.by_category('', project).map(&:name)
expect(all).to include('bug')
expect(all).to include('feature_proposal')
- expect(all).to include('template_test')
end
context 'when repo is bare or empty' do
let(:empty_project) { create(:project) }
- before do
- empty_project.add_user(user, Gitlab::Access::MASTER)
- end
-
it "returns empty array" do
- templates = subject.by_category('', empty_project)
+ templates = described_class.by_category('', empty_project)
+
expect(templates).to be_empty
end
end
@@ -66,26 +49,23 @@ describe Gitlab::Template::MergeRequestTemplate do
describe '#content' do
it 'loads the full file' do
- issue_template = subject.new('.gitlab/merge_request_templates/bug.md', project)
+ issue_template = described_class.new('.gitlab/merge_request_templates/bug.md', project)
expect(issue_template.name).to eq 'bug'
expect(issue_template.content).to eq('something valid')
end
it 'raises error when file is not found' do
- issue_template = subject.new('.gitlab/merge_request_templates/bugnot.md', project)
+ issue_template = described_class.new('.gitlab/merge_request_templates/bugnot.md', project)
expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
end
context "when repo is empty" do
let(:empty_project) { create(:project) }
- before do
- empty_project.add_user(user, Gitlab::Access::MASTER)
- end
-
it "raises file not found" do
- issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project)
+ issue_template = described_class.new('.gitlab/merge_request_templates/not_existent.md', empty_project)
+
expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
end
end
diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb
new file mode 100644
index 00000000000..ecacea6bb35
--- /dev/null
+++ b/spec/lib/gitlab/themes_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Themes, lib: true do
+ describe '.body_classes' do
+ it 'returns a space-separated list of class names' do
+ css = described_class.body_classes
+
+ expect(css).to include('ui_indigo')
+ expect(css).to include(' ui_dark ')
+ expect(css).to include(' ui_blue')
+ end
+ end
+
+ describe '.by_id' do
+ it 'returns a Theme by its ID' do
+ expect(described_class.by_id(1).name).to eq 'Indigo'
+ expect(described_class.by_id(3).name).to eq 'Light'
+ end
+ end
+
+ describe '.default' do
+ it 'returns the default application theme' do
+ allow(described_class).to receive(:default_id).and_return(2)
+ expect(described_class.default.id).to eq 2
+ end
+
+ it 'prevents an infinite loop when configuration default is invalid' do
+ default = described_class::APPLICATION_DEFAULT
+ themes = described_class::THEMES
+
+ config = double(default_theme: 0).as_null_object
+ allow(Gitlab).to receive(:config).and_return(config)
+ expect(described_class.default.id).to eq default
+
+ config = double(default_theme: themes.size + 5).as_null_object
+ allow(Gitlab).to receive(:config).and_return(config)
+ expect(described_class.default.id).to eq default
+ end
+ end
+
+ describe '.each' do
+ it 'passes the block to the THEMES Array' do
+ ids = []
+ described_class.each { |theme| ids << theme.id }
+ expect(ids).not_to be_empty
+ end
+ end
+end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index f5b4882815f..f18823b61ef 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -20,6 +20,34 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git')).to be true
end
+ it 'returns true for a non-alphanumeric hostname' do
+ stub_resolv
+
+ aggregate_failures do
+ expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami/a')
+
+ # The leading character here is a Unicode "soft hyphen"
+ expect(described_class).to be_blocked_url('ssh://­oProxyCommand=whoami/a')
+
+ # Unicode alphanumerics are allowed
+ expect(described_class).not_to be_blocked_url('ssh://ÄŸitlab.com/a')
+ end
+ end
+
+ it 'returns true for a non-alphanumeric username' do
+ stub_resolv
+
+ aggregate_failures do
+ expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a')
+
+ # The leading character here is a Unicode "soft hyphen"
+ expect(described_class).to be_blocked_url('ssh://­oProxyCommand=whoami@example.com/a')
+
+ # Unicode alphanumerics are allowed
+ expect(described_class).not_to be_blocked_url('ssh://ÄŸitlab@example.com/a')
+ end
+ end
+
it 'returns true for invalid URL' do
expect(described_class.blocked_url?('http://:8080')).to be true
end
@@ -28,4 +56,10 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
end
end
+
+ # Resolv does not support resolving UTF-8 domain names
+ # See https://bugs.ruby-lang.org/issues/4270
+ def stub_resolv
+ allow(Resolv).to receive(:getaddresses).and_return([])
+ end
end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 308b1a128be..fdc3990132a 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -1,11 +1,7 @@
require 'spec_helper'
describe Gitlab::UrlSanitizer do
- let(:credentials) { { user: 'blah', password: 'password' } }
- let(:url_sanitizer) do
- described_class.new("https://github.com/me/project.git", credentials: credentials)
- end
- let(:user) { double(:user, username: 'john.doe') }
+ using RSpec::Parameterized::TableSyntax
describe '.sanitize' do
def sanitize_url(url)
@@ -16,83 +12,166 @@ describe Gitlab::UrlSanitizer do
})
end
- it 'mask the credentials from HTTP URLs' do
- filtered_content = sanitize_url('http://user:pass@test.com/root/repoC.git/')
+ where(:input, :output) do
+ 'http://user:pass@test.com/root/repoC.git/' | 'http://*****:*****@test.com/root/repoC.git/'
+ 'https://user:pass@test.com/root/repoA.git/' | 'https://*****:*****@test.com/root/repoA.git/'
+ 'ssh://user@host.test/path/to/repo.git' | 'ssh://*****@host.test/path/to/repo.git'
- expect(filtered_content).to include("http://*****:*****@test.com/root/repoC.git/")
- end
+ # git protocol does not support authentication but clean any details anyway
+ 'git://user:pass@host.test/path/to/repo.git' | 'git://*****:*****@host.test/path/to/repo.git'
+ 'git://host.test/path/to/repo.git' | 'git://host.test/path/to/repo.git'
- it 'mask the credentials from HTTPS URLs' do
- filtered_content = sanitize_url('https://user:pass@test.com/root/repoA.git/')
+ # SCP-style URLs are left unmodified
+ 'user@server:project.git' | 'user@server:project.git'
+ 'user:pass@server:project.git' | 'user:pass@server:project.git'
- expect(filtered_content).to include("https://*****:*****@test.com/root/repoA.git/")
+ # return an empty string for invalid URLs
+ 'ssh://' | ''
end
- it 'mask credentials from SSH URLs' do
- filtered_content = sanitize_url('ssh://user@host.test/path/to/repo.git')
-
- expect(filtered_content).to include("ssh://*****@host.test/path/to/repo.git")
+ with_them do
+ it { expect(sanitize_url(input)).to include("repository '#{output}' not found") }
end
+ end
- it 'does not modify Git URLs' do
- # git protocol does not support authentication
- filtered_content = sanitize_url('git://host.test/path/to/repo.git')
+ describe '.valid?' do
+ where(:value, :url) do
+ false | nil
+ false | ''
+ false | '123://invalid:url'
+ true | 'valid@project:url.git'
+ true | 'ssh://example.com'
+ true | 'ssh://:@example.com'
+ true | 'ssh://foo@example.com'
+ true | 'ssh://foo:bar@example.com'
+ true | 'ssh://foo:bar@example.com/group/group/project.git'
+ true | 'git://example.com/group/group/project.git'
+ true | 'git://foo:bar@example.com/group/group/project.git'
+ true | 'http://foo:bar@example.com/group/group/project.git'
+ true | 'https://foo:bar@example.com/group/group/project.git'
+ end
- expect(filtered_content).to include("git://host.test/path/to/repo.git")
+ with_them do
+ it { expect(described_class.valid?(url)).to eq(value) }
end
+ end
+
+ describe '#sanitized_url' do
+ context 'credentials in hash' do
+ where(username: ['foo', '', nil], password: ['bar', '', nil])
- it 'does not modify scp-like URLs' do
- filtered_content = sanitize_url('user@server:project.git')
+ with_them do
+ let(:credentials) { { user: username, password: password } }
+ subject { described_class.new('http://example.com', credentials: credentials).sanitized_url }
- expect(filtered_content).to include("user@server:project.git")
+ it { is_expected.to eq('http://example.com') }
+ end
end
- it 'returns an empty string for invalid URLs' do
- filtered_content = sanitize_url('ssh://')
+ context 'credentials in URL' do
+ where(userinfo: %w[foo:bar@ foo@ :bar@ :@ @] + [nil])
- expect(filtered_content).to include("repository '' not found")
- end
- end
+ with_them do
+ subject { described_class.new("http://#{userinfo}example.com").sanitized_url }
- describe '.valid?' do
- it 'validates url strings' do
- expect(described_class.valid?(nil)).to be(false)
- expect(described_class.valid?('valid@project:url.git')).to be(true)
- expect(described_class.valid?('123://invalid:url')).to be(false)
+ it { is_expected.to eq('http://example.com') }
+ end
end
end
- describe '#sanitized_url' do
- it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") }
- end
-
describe '#credentials' do
- it { expect(url_sanitizer.credentials).to eq(credentials) }
+ 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
- context 'when user is given to #initialize' do
- let(:url_sanitizer) do
- described_class.new("https://github.com/me/project.git", credentials: { user: user.username })
+ with_them do
+ subject { described_class.new('user@example.com:path.git', credentials: input).credentials }
+
+ it { is_expected.to eq(output) }
end
- it { expect(url_sanitizer.credentials).to eq({ user: 'john.doe' }) }
+ it 'overrides URL-provided credentials' do
+ sanitizer = described_class.new('http://a:b@example.com', credentials: { user: 'c', password: 'd' })
+
+ expect(sanitizer.credentials).to eq(user: 'c', password: 'd')
+ end
+ end
+
+ context 'credentials in URL' do
+ where(:url, :credentials) do
+ 'http://foo:bar@example.com' | { user: 'foo', password: 'bar' }
+ 'http://:bar@example.com' | { user: nil, password: 'bar' }
+ 'http://foo:@example.com' | { user: 'foo', password: nil }
+ 'http://foo@example.com' | { user: 'foo', password: nil }
+ 'http://:@example.com' | { user: nil, password: nil }
+ '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 }
+ 'no' | { user: nil, password: nil }
+ end
+
+ with_them do
+ subject { described_class.new(url).credentials }
+
+ it { is_expected.to eq(credentials) }
+ end
end
end
describe '#full_url' do
- it { expect(url_sanitizer.full_url).to eq("https://blah:password@github.com/me/project.git") }
+ context 'credentials in hash' do
+ where(:credentials, :userinfo) do
+ { user: 'foo', password: 'bar' } | 'foo:bar@'
+ { user: 'foo', password: '' } | 'foo@'
+ { user: 'foo', password: nil } | 'foo@'
+ { user: '', password: 'bar' } | ':bar@'
+ { user: '', password: '' } | nil
+ { user: '', password: nil } | nil
+ { user: nil, password: 'bar' } | ':bar@'
+ { user: nil, password: '' } | nil
+ { user: nil, password: nil } | nil
+ end
- it 'supports scp-like URLs' do
- sanitizer = described_class.new('user@server:project.git')
+ with_them do
+ subject { described_class.new('http://example.com', credentials: credentials).full_url }
- expect(sanitizer.full_url).to eq('user@server:project.git')
+ it { is_expected.to eq("http://#{userinfo}example.com") }
+ end
end
- context 'when user is given to #initialize' do
- let(:url_sanitizer) do
- described_class.new("https://github.com/me/project.git", credentials: { user: user.username })
+ context 'credentials in URL' do
+ where(:input, :output) do
+ nil | ''
+ '' | :same
+ 'git@example.com' | :same
+ 'http://example.com' | :same
+ 'http://foo@example.com' | :same
+ 'http://foo:@example.com' | 'http://foo@example.com'
+ 'http://:bar@example.com' | :same
+ 'http://foo:bar@example.com' | :same
end
- it { expect(url_sanitizer.full_url).to eq("https://john.doe@github.com/me/project.git") }
+ with_them do
+ let(:expected) { output == :same ? input : output }
+
+ it { expect(described_class.new(input).full_url).to eq(expected) }
+ end
end
end
end
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 111c873f79c..3137a72fdc4 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -1,7 +1,21 @@
require 'spec_helper'
describe Gitlab::Utils do
- delegate :to_boolean, :boolean_to_yes_no, to: :described_class
+ delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, to: :described_class
+
+ describe '.slugify' do
+ {
+ 'TEST' => 'test',
+ 'project_with_underscores' => 'project-with-underscores',
+ 'namespace/project' => 'namespace-project',
+ 'a' * 70 => 'a' * 63,
+ 'test_trailing_' => 'test-trailing'
+ }.each do |original, expected|
+ it "slugifies #{original} to #{expected}" do
+ expect(slugify(original)).to eq(expected)
+ end
+ end
+ end
describe '.to_boolean' do
it 'accepts booleans' do
@@ -39,4 +53,10 @@ describe Gitlab::Utils do
expect(boolean_to_yes_no(false)).to eq('No')
end
end
+
+ describe '.random_string' do
+ it 'generates a string' do
+ expect(random_string).to be_kind_of(String)
+ end
+ end
end
diff --git a/spec/lib/gitlab/version_info_spec.rb b/spec/lib/gitlab/version_info_spec.rb
index e7e1a92ae54..c8a1e433d59 100644
--- a/spec/lib/gitlab/version_info_spec.rb
+++ b/spec/lib/gitlab/version_info_spec.rb
@@ -50,8 +50,8 @@ describe 'Gitlab::VersionInfo' do
context 'unknown' do
it { expect(@unknown).not_to be @v0_0_1 }
it { expect(@unknown).not_to be Gitlab::VersionInfo.new }
- it { expect{@unknown > @v0_0_1}.to raise_error(ArgumentError) }
- it { expect{@unknown < @v0_0_1}.to raise_error(ArgumentError) }
+ it { expect {@unknown > @v0_0_1}.to raise_error(ArgumentError) }
+ it { expect {@unknown < @v0_0_1}.to raise_error(ArgumentError) }
end
context 'parse' do
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 654397ccffb..699184ad9fe 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -202,7 +202,6 @@ describe Gitlab::Workhorse do
context 'when Gitaly is enabled' do
let(:gitaly_params) do
{
- GitalyAddress: Gitlab::GitalyClient.address('default'),
GitalyServer: {
address: Gitlab::GitalyClient.address('default'),
token: Gitlab::GitalyClient.token('default')
@@ -217,7 +216,9 @@ describe Gitlab::Workhorse do
it 'includes a Repository param' do
repo_param = { Repository: {
storage_name: 'default',
- relative_path: project.full_path + '.git'
+ relative_path: project.full_path + '.git',
+ git_object_directory: '',
+ git_alternate_object_directories: []
} }
expect(subject).to include(repo_param)
@@ -227,21 +228,10 @@ describe Gitlab::Workhorse do
let(:action) { 'git_upload_pack' }
let(:feature_flag) { :post_upload_pack }
- context 'when action is enabled by feature flag' do
- it 'includes Gitaly params in the returned value' do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(true)
+ it 'includes Gitaly params in the returned value' do
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(true)
- expect(subject).to include(gitaly_params)
- end
- end
-
- context 'when action is not enabled by feature flag' do
- it 'does not include Gitaly params in the returned value' do
- status_opt_out = Gitlab::GitalyClient::MigrationStatus::OPT_OUT
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag, status: status_opt_out).and_return(false)
-
- expect(subject).not_to include(gitaly_params)
- end
+ expect(subject).to include(gitaly_params)
end
end
diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb
index e7022bd06f8..d6edc964844 100644
--- a/spec/lib/json_web_token/rsa_token_spec.rb
+++ b/spec/lib/json_web_token/rsa_token_spec.rb
@@ -27,7 +27,7 @@ describe JSONWebToken::RSAToken do
subject { JWT.decode(rsa_encoded, rsa_key) }
- it { expect{subject}.not_to raise_error }
+ it { expect {subject}.not_to raise_error }
it { expect(subject.first).to include('key' => 'value') }
it do
expect(subject.second).to eq(
@@ -41,7 +41,7 @@ describe JSONWebToken::RSAToken do
let(:new_key) { OpenSSL::PKey::RSA.generate(512) }
subject { JWT.decode(rsa_encoded, new_key) }
- it { expect{subject}.to raise_error(JWT::DecodeError) }
+ it { expect {subject}.to raise_error(JWT::DecodeError) }
end
end
end
diff --git a/spec/lib/rspec_flaky/example_spec.rb b/spec/lib/rspec_flaky/example_spec.rb
new file mode 100644
index 00000000000..5b4fd5ddf3e
--- /dev/null
+++ b/spec/lib/rspec_flaky/example_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe RspecFlaky::Example do
+ let(: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: 'BOOM!'),
+ attempts: 1
+ }
+ end
+ let(:rspec_example) { double(example_attrs) }
+
+ describe '#initialize' do
+ shared_examples 'a valid Example instance' do
+ it 'returns valid attributes' do
+ example = described_class.new(args)
+
+ expect(example.example_id).to eq(example_attrs[:id])
+ end
+ end
+
+ context 'when given an Rspec::Core::Example that responds to #example' do
+ let(:args) { double(example: rspec_example) }
+
+ it_behaves_like 'a valid Example instance'
+ end
+
+ context 'when given an Rspec::Core::Example that does not respond to #example' do
+ let(:args) { rspec_example }
+
+ it_behaves_like 'a valid Example instance'
+ end
+ end
+
+ subject { described_class.new(rspec_example) }
+
+ describe '#uid' do
+ it 'returns a hash of the full description' do
+ expect(subject.uid).to eq(Digest::MD5.hexdigest("#{subject.description}-#{subject.file}"))
+ end
+ end
+
+ describe '#example_id' do
+ it 'returns the ID of the RSpec::Core::Example' do
+ expect(subject.example_id).to eq(rspec_example.id)
+ end
+ end
+
+ describe '#attempts' do
+ it 'returns the attempts of the RSpec::Core::Example' do
+ expect(subject.attempts).to eq(rspec_example.attempts)
+ end
+ end
+
+ describe '#file' do
+ it 'returns the metadata[:file_path] of the RSpec::Core::Example' do
+ expect(subject.file).to eq(rspec_example.metadata[:file_path])
+ end
+ end
+
+ describe '#line' do
+ it 'returns the metadata[:line_number] of the RSpec::Core::Example' do
+ expect(subject.line).to eq(rspec_example.metadata[:line_number])
+ end
+ end
+
+ describe '#description' do
+ it 'returns the metadata[:full_description] of the RSpec::Core::Example' do
+ expect(subject.description).to eq(rspec_example.metadata[:full_description])
+ end
+ end
+
+ describe '#status' do
+ it 'returns the execution_result.status of the RSpec::Core::Example' do
+ expect(subject.status).to eq(rspec_example.execution_result.status)
+ end
+ end
+
+ describe '#exception' do
+ it 'returns the execution_result.exception of the RSpec::Core::Example' do
+ expect(subject.exception).to eq(rspec_example.execution_result.exception)
+ end
+ end
+end
diff --git a/spec/lib/rspec_flaky/flaky_example_spec.rb b/spec/lib/rspec_flaky/flaky_example_spec.rb
new file mode 100644
index 00000000000..cbfc1e538ab
--- /dev/null
+++ b/spec/lib/rspec_flaky/flaky_example_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe RspecFlaky::FlakyExample do
+ let(:flaky_example_attrs) 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_flaky_at: 2345,
+ last_attempts_count: 2,
+ flaky_reports: 1
+ }
+ end
+ let(:example_attrs) do
+ {
+ uid: 'abc123',
+ example_id: flaky_example_attrs[:example_id],
+ file: flaky_example_attrs[:file],
+ line: flaky_example_attrs[:line],
+ description: flaky_example_attrs[:description],
+ status: 'passed',
+ exception: 'BOOM!',
+ attempts: flaky_example_attrs[:last_attempts_count]
+ }
+ end
+ let(:example) { double(example_attrs) }
+
+ describe '#initialize' do
+ shared_examples 'a valid FlakyExample instance' do
+ it 'returns valid attributes' do
+ flaky_example = described_class.new(args)
+
+ expect(flaky_example.uid).to eq(flaky_example_attrs[:uid])
+ expect(flaky_example.example_id).to eq(flaky_example_attrs[:example_id])
+ end
+ end
+
+ context 'when given an Rspec::Example' do
+ let(:args) { example }
+
+ it_behaves_like 'a valid FlakyExample instance'
+ end
+
+ context 'when given a hash' do
+ let(:args) { flaky_example_attrs }
+
+ it_behaves_like 'a valid FlakyExample instance'
+ 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
+
+ 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(last_flaky_at: instance_of(Time), last_flaky_job: nil)
+ .merge(additional_attrs)
+
+ expect(flaky_example.to_h).to match(hash_including(final_hash))
+ end
+ end
+
+ context 'when given an Rspec::Example' do
+ let(:args) { example }
+
+ context 'when run locally' do
+ it_behaves_like 'a valid FlakyExample hash' do
+ let(:additional_attrs) do
+ { first_flaky_at: instance_of(Time) }
+ end
+ end
+ end
+
+ context 'when run on the CI' do
+ before do
+ stub_env('CI_PROJECT_URL', 'https://gitlab.com/gitlab-org/gitlab-ce')
+ 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
+ end
+ end
+ end
+
+ context 'when given a hash' do
+ let(:args) { flaky_example_attrs }
+
+ it_behaves_like 'a valid FlakyExample hash'
+ end
+ end
+end
diff --git a/spec/lib/rspec_flaky/listener_spec.rb b/spec/lib/rspec_flaky/listener_spec.rb
new file mode 100644
index 00000000000..0e193bf408b
--- /dev/null
+++ b/spec/lib/rspec_flaky/listener_spec.rb
@@ -0,0 +1,178 @@
+require 'spec_helper'
+
+describe RspecFlaky::Listener do
+ let(:flaky_example_report) do
+ {
+ 'abc123' => {
+ 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,
+ flaky_reports: 1,
+ last_flaky_job: nil
+ }
+ }
+ end
+ let(:example_attrs) do
+ {
+ id: 'spec/foo/baz_spec.rb:3',
+ metadata: {
+ file_path: 'spec/foo/baz_spec.rb',
+ line_number: 3,
+ full_description: 'hello GitLab'
+ },
+ execution_result: double(status: 'passed', exception: nil)
+ }
+ end
+
+ 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 Listener instance' do
+ let(:expected_all_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({})
+ end
+ end
+
+ context 'when no report file exists' do
+ it_behaves_like 'a valid Listener instance'
+ end
+
+ context 'when a report file exists and set by ALL_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.rewind
+ end
+ end
+
+ before do
+ stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file.path)
+ end
+
+ after do
+ report_file.close
+ report_file.unlink
+ end
+
+ it_behaves_like 'a valid Listener instance' do
+ let(:expected_all_flaky_examples) { flaky_example_report }
+ end
+ end
+ end
+
+ describe '#example_passed' do
+ let(:rspec_example) { double(example_attrs) }
+ let(:notification) { double(example: rspec_example) }
+
+ 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 }
+ 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(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(example_attrs.merge(attempts: 2)) }
+ let(:expected_new_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 }
+
+ 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))
+ end
+ end
+ end
+
+ describe '#dump_summary' do
+ let(:rspec_example) { double(example_attrs) }
+ let(:notification) { double(example: rspec_example) }
+
+ context 'when a report file path is set by ALL_FLAKY_RSPEC_REPORT_PATH' do
+ let(:report_file_path) { Rails.root.join('tmp', 'rspec_flaky_report.json') }
+
+ before do
+ stub_env('ALL_FLAKY_RSPEC_REPORT_PATH', report_file_path)
+ FileUtils.rm(report_file_path) if File.exist?(report_file_path)
+ end
+
+ after do
+ FileUtils.rm(report_file_path) if File.exist?(report_file_path)
+ end
+
+ context 'when FLAKY_RSPEC_GENERATE_REPORT == "false"' do
+ before do
+ stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'false')
+ end
+
+ it 'does not write the report file' do
+ subject.example_passed(notification)
+
+ subject.dump_summary(nil)
+
+ expect(File.exist?(report_file_path)).to be(false)
+ end
+ end
+
+ context 'when FLAKY_RSPEC_GENERATE_REPORT == "true"' do
+ before do
+ stub_env('FLAKY_RSPEC_GENERATE_REPORT', 'true')
+ end
+
+ it 'writes the report file' do
+ subject.example_passed(notification)
+
+ subject.dump_summary(nil)
+
+ expect(File.exist?(report_file_path)).to be(true)
+ end
+ end
+ end
+ end
+
+ describe '#to_report' do
+ 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))
+ 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
new file mode 100644
index 00000000000..7125bfcab59
--- /dev/null
+++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe SystemCheck::App::GitUserDefaultSSHConfigCheck do
+ let(:username) { '_this_user_will_not_exist_unless_it_is_stubbed' }
+ let(:base_dir) { Dir.mktmpdir }
+ let(:home_dir) { File.join(base_dir, "/var/lib/#{username}") }
+ let(:ssh_dir) { File.join(home_dir, '.ssh') }
+ let(:forbidden_file) { 'id_rsa' }
+
+ before do
+ allow(Gitlab.config.gitlab).to receive(:user).and_return(username)
+ end
+
+ after do
+ FileUtils.rm_rf(base_dir)
+ end
+
+ it 'only whitelists safe files' do
+ expect(described_class::WHITELIST).to contain_exactly('authorized_keys', 'authorized_keys2', 'known_hosts')
+ end
+
+ describe '#skip?' do
+ subject { described_class.new.skip? }
+
+ where(user_exists: [true, false], home_dir_exists: [true, false])
+
+ with_them do
+ let(:expected_result) { !user_exists || !home_dir_exists }
+
+ before do
+ stub_user if user_exists
+ stub_home_dir if home_dir_exists
+ end
+
+ it { is_expected.to eq(expected_result) }
+ end
+ end
+
+ describe '#check?' do
+ subject { described_class.new.check? }
+
+ before do
+ stub_user
+ end
+
+ it 'fails if a forbidden file exists' do
+ stub_ssh_file(forbidden_file)
+
+ is_expected.to be_falsy
+ end
+
+ it "succeeds if the SSH directory doesn't exist" do
+ FileUtils.rm_rf(ssh_dir)
+
+ is_expected.to be_truthy
+ end
+
+ it 'succeeds if all the whitelisted files exist' do
+ described_class::WHITELIST.each do |filename|
+ stub_ssh_file(filename)
+ end
+
+ is_expected.to be_truthy
+ end
+ end
+
+ def stub_user
+ allow(File).to receive(:expand_path).with("~#{username}").and_return(home_dir)
+ end
+
+ def stub_home_dir
+ FileUtils.mkdir_p(home_dir)
+ end
+
+ def stub_ssh_file(filename)
+ FileUtils.mkdir_p(ssh_dir)
+ FileUtils.touch(File.join(ssh_dir, filename))
+ end
+end
diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb
index 025ea2673b4..9da3648400e 100644
--- a/spec/lib/system_check/simple_executor_spec.rb
+++ b/spec/lib/system_check/simple_executor_spec.rb
@@ -35,6 +35,20 @@ describe SystemCheck::SimpleExecutor do
end
end
+ class DynamicSkipCheck < SystemCheck::BaseCheck
+ set_name 'dynamic skip check'
+ set_skip_reason 'this is a skip reason'
+
+ def skip?
+ self.skip_reason = 'this is a dynamic skip reason'
+ true
+ end
+
+ def check?
+ raise 'should not execute this'
+ end
+ end
+
class MultiCheck < SystemCheck::BaseCheck
set_name 'multi check'
@@ -127,6 +141,10 @@ describe SystemCheck::SimpleExecutor do
expect(subject.checks.size).to eq(1)
end
+
+ it 'errors out when passing multiple items' do
+ expect { subject << [SimpleCheck, OtherCheck] }.to raise_error(ArgumentError)
+ end
end
subject { described_class.new('Test') }
@@ -205,10 +223,14 @@ describe SystemCheck::SimpleExecutor do
subject.run_check(SkipCheck)
end
- it 'displays #skip_reason' do
+ it 'displays .skip_reason' do
expect { subject.run_check(SkipCheck) }.to output(/this is a skip reason/).to_stdout
end
+ it 'displays #skip_reason' do
+ expect { subject.run_check(DynamicSkipCheck) }.to output(/this is a dynamic skip reason/).to_stdout
+ end
+
it 'does not execute #check when #skip? is true' do
expect_any_instance_of(SkipCheck).not_to receive(:check?)
@@ -240,7 +262,7 @@ describe SystemCheck::SimpleExecutor do
context 'when there is an exception' do
it 'rescues the exception' do
- expect{ subject.run_check(BugousCheck) }.not_to raise_exception
+ expect { subject.run_check(BugousCheck) }.not_to raise_exception
end
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index e36d7a1800c..932e2fd8c95 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -8,6 +8,25 @@ describe Notify do
include_context 'gitlab email notification'
+ set(:user) { create(:user) }
+ set(:current_user) { create(:user, email: "current@email.com") }
+ set(:assignee) { create(:user, email: 'assignee@example.com', name: 'John Doe') }
+
+ set(:merge_request) do
+ create(:merge_request, source_project: project,
+ target_project: project,
+ author: current_user,
+ assignee: assignee,
+ description: 'Awesome description')
+ end
+
+ set(:issue) do
+ create(:issue, author: current_user,
+ assignees: [assignee],
+ project: project,
+ description: 'My awesome description!')
+ end
+
def have_referable_subject(referable, reply: false)
prefix = referable.project.name if referable.project
prefix = "Re: #{prefix}" if reply
@@ -19,8 +38,6 @@ describe Notify do
context 'for a project' do
describe 'items that are assignable, the email' do
- let(:current_user) { create(:user, email: "current@email.com") }
- let(:assignee) { create(:user, email: 'assignee@example.com', name: 'John Doe') }
let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
shared_examples 'an assignee email' do
@@ -36,9 +53,6 @@ describe Notify do
end
context 'for issues' do
- let(:issue) { create(:issue, author: current_user, assignees: [assignee], project: project) }
- let(:issue_with_description) { create(:issue, author: current_user, assignees: [assignee], project: project, description: 'My awesome description') }
-
describe 'that are new' do
subject { described_class.new_issue_email(issue.assignees.first.id, issue.id) }
@@ -56,6 +70,10 @@ describe Notify do
end
end
+ it 'contains the description' do
+ is_expected.to have_html_escaped_body_text issue.description
+ end
+
context 'when enabled email_author_in_body' do
before do
stub_application_setting(email_author_in_body: true)
@@ -68,16 +86,6 @@ describe Notify do
end
end
- describe 'that are new with a description' do
- subject { described_class.new_issue_email(issue_with_description.assignees.first.id, issue_with_description.id) }
-
- it_behaves_like 'it should show Gmail Actions View Issue link'
-
- it 'contains the description' do
- is_expected.to have_html_escaped_body_text issue_with_description.description
- end
- end
-
describe 'that have been reassigned' do
subject { described_class.reassigned_issue_email(recipient.id, issue.id, [previous_assignee.id], current_user.id) }
@@ -197,11 +205,6 @@ describe Notify do
end
context 'for merge requests' do
- let(:project) { create(:project, :repository) }
- let(:merge_author) { create(:user) }
- let(:merge_request) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project) }
- let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: 'My awesome description') }
-
describe 'that are new' do
subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
@@ -221,6 +224,10 @@ describe Notify do
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)
@@ -233,17 +240,6 @@ describe Notify do
end
end
- describe 'that are new with a description' do
- subject { described_class.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) }
-
- it_behaves_like 'it should show Gmail Actions View Merge request link'
- it_behaves_like "an unsubscribeable thread"
-
- it 'contains the description' do
- is_expected.to have_html_escaped_body_text merge_request_with_description.description
- end
- end
-
describe 'that are reassigned' do
subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
@@ -321,6 +317,7 @@ describe Notify do
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) }
it_behaves_like 'a multiple recipients email'
@@ -348,8 +345,6 @@ describe Notify do
end
describe 'project was moved' do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
subject { described_class.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
it_behaves_like 'an email sent from GitLab'
@@ -371,7 +366,6 @@ describe Notify do
end
end
- let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
project.requesters.find_by(user_id: user.id)
@@ -398,7 +392,6 @@ describe Notify do
let(:group_owner) { create(:user) }
let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } }
let(:project) { create(:project, :public, :access_requestable, namespace: group) }
- let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
project.requesters.find_by(user_id: user.id)
@@ -424,7 +417,6 @@ describe Notify do
describe 'project access denied' do
let(:project) { create(:project, :public, :access_requestable) }
- let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
project.requesters.find_by(user_id: user.id)
@@ -445,7 +437,6 @@ describe Notify do
describe 'project access changed' do
let(:owner) { create(:user, name: "Chang O'Keefe") }
let(:project) { create(:project, :public, :access_requestable, namespace: owner.namespace) }
- let(:user) { create(:user) }
let(:project_member) { create(:project_member, project: project, user: user) }
subject { described_class.member_access_granted_email('project', project_member.id) }
@@ -474,7 +465,6 @@ describe Notify do
end
describe 'project invitation' do
- let(:project) { create(:project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) { invite_to_project(project, inviter: master) }
@@ -494,7 +484,6 @@ describe Notify do
end
describe 'project invitation accepted' do
- let(:project) { create(:project) }
let(:invited_user) { create(:user, name: 'invited user') }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do
@@ -519,7 +508,6 @@ describe Notify do
end
describe 'project invitation declined' do
- let(:project) { create(:project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do
invitee = invite_to_project(project, inviter: master)
@@ -545,7 +533,7 @@ describe Notify do
let(:note_author) { create(:user, name: 'author_name') }
let(:note) { create(:note, project: project, author: note_author) }
- before :each do
+ before do
allow(Note).to receive(:find).with(note.id).and_return(note)
end
@@ -582,7 +570,6 @@ describe Notify do
end
describe 'on a commit' do
- let(:project) { create(:project, :repository) }
let(:commit) { project.commit }
before do
@@ -607,7 +594,6 @@ describe Notify do
end
describe 'on a merge request' do
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:note_on_merge_request_path) { project_merge_request_path(project, merge_request, anchor: "note_#{note.id}") }
before do
@@ -632,7 +618,6 @@ describe Notify do
end
describe 'on an issue' do
- let(:issue) { create(:issue, project: project) }
let(:note_on_issue_path) { project_issue_path(project, issue, anchor: "note_#{note.id}") }
before do
@@ -658,10 +643,9 @@ describe Notify do
end
context 'items that are noteable, the email for a discussion note' do
- let(:project) { create(:project, :repository) }
let(:note_author) { create(:user, name: 'author_name') }
- before :each do
+ before do
allow(Note).to receive(:find).with(note.id).and_return(note)
end
@@ -722,7 +706,6 @@ describe Notify do
end
describe 'on a merge request' do
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project, author: note_author) }
let(:note_on_merge_request_path) { project_merge_request_path(project, merge_request, anchor: "note_#{note.id}") }
@@ -749,7 +732,6 @@ describe Notify do
end
describe 'on an issue' do
- let(:issue) { create(:issue, project: project) }
let(:note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: note_author) }
let(:note_on_issue_path) { project_issue_path(project, issue, anchor: "note_#{note.id}") }
@@ -779,7 +761,7 @@ describe Notify do
context 'items that are noteable, the email for a diff discussion note' do
let(:note_author) { create(:user, name: 'author_name') }
- before :each do
+ before do
allow(Note).to receive(:find).with(note.id).and_return(note)
end
@@ -835,7 +817,6 @@ describe Notify do
end
describe 'on a merge request' do
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:note) { create(:diff_note_on_merge_request) }
subject { described_class.note_merge_request_email(recipient.id, note.id) }
@@ -848,9 +829,10 @@ describe Notify do
end
context 'for a group' do
+ set(:group) { create(:group) }
+
describe 'group access requested' do
let(:group) { create(:group, :public, :access_requestable) }
- let(:user) { create(:user) }
let(:group_member) do
group.request_access(user)
group.requesters.find_by(user_id: user.id)
@@ -870,8 +852,6 @@ describe Notify do
end
describe 'group access denied' do
- let(:group) { create(:group) }
- let(:user) { create(:user) }
let(:group_member) do
group.request_access(user)
group.requesters.find_by(user_id: user.id)
@@ -890,8 +870,6 @@ describe Notify do
end
describe 'group access changed' do
- let(:group) { create(:group) }
- let(:user) { create(:user) }
let(:group_member) { create(:group_member, group: group, user: user) }
subject { described_class.member_access_granted_email('group', group_member.id) }
@@ -921,7 +899,6 @@ describe Notify do
end
describe 'group invitation' do
- let(:group) { create(:group) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) { invite_to_group(group, inviter: owner) }
@@ -941,7 +918,6 @@ describe Notify do
end
describe 'group invitation accepted' do
- let(:group) { create(:group) }
let(:invited_user) { create(:user, name: 'invited user') }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) do
@@ -966,7 +942,6 @@ describe Notify do
end
describe 'group invitation declined' do
- let(:group) { create(:group) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) do
invitee = invite_to_group(group, inviter: owner)
@@ -1020,7 +995,6 @@ describe Notify do
describe 'email on push for a created branch' do
let(:example_site_path) { root_path }
- let(:user) { create(:user) }
let(:tree_path) { project_tree_path(project, "empty-branch") }
subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/empty-branch', action: :create) }
@@ -1046,7 +1020,6 @@ describe Notify do
describe 'email on push for a created tag' do
let(:example_site_path) { root_path }
- let(:user) { create(:user) }
let(:tree_path) { project_tree_path(project, "v1.0") }
subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :create) }
@@ -1072,7 +1045,6 @@ describe Notify do
describe 'email on push for a deleted branch' do
let(:example_site_path) { root_path }
- let(:user) { create(:user) }
subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/heads/master', action: :delete) }
@@ -1094,7 +1066,6 @@ describe Notify do
describe 'email on push for a deleted tag' do
let(:example_site_path) { root_path }
- let(:user) { create(:user) }
subject { described_class.repository_push_email(project.id, author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
@@ -1115,9 +1086,7 @@ describe Notify do
end
describe 'email on push with multiple commits' do
- let(:project) { create(:project, :repository) }
let(:example_site_path) { root_path }
- let(:user) { create(:user) }
let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) }
let(:compare) { Compare.decorate(raw_compare, project) }
let(:commits) { compare.commits }
@@ -1209,9 +1178,7 @@ describe Notify do
end
describe 'email on push with a single commit' do
- let(:project) { create(:project, :repository) }
let(:example_site_path) { root_path }
- let(:user) { create(:user) }
let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) }
let(:compare) { Compare.decorate(raw_compare, project) }
let(:commits) { compare.commits }
@@ -1242,8 +1209,6 @@ describe Notify do
end
describe 'HTML emails setting' do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
let(:multipart_mail) { described_class.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
context 'when disabled' do
diff --git a/spec/migrations/README.md b/spec/migrations/README.md
index 05d4f35db72..45cf25b96de 100644
--- a/spec/migrations/README.md
+++ b/spec/migrations/README.md
@@ -28,6 +28,14 @@ The `after` hook will migrate the database **up** and reinstitutes the latest
schema version, so that the process does not affect subsequent specs and
ensures proper isolation.
+## Testing a class that is not an ActiveRecord::Migration
+
+In order to test a class that is not a migration itself, you will need to
+manually provide a required schema version. Please add a `schema` tag to a
+context that you want to switch the database schema within.
+
+Example: `describe SomeClass, :migration, schema: 20170608152748`.
+
## Available helpers
Use `table` helper to create a temporary `ActiveRecord::Base` derived model
@@ -80,8 +88,6 @@ end
## Best practices
-1. Use only one test example per migration unless there is a good reason to
-use more.
1. Note that this type of tests do not run within the transaction, we use
a truncation database cleanup strategy. Do not depend on transaction being
present.
diff --git a/spec/migrations/clean_upload_symlinks_spec.rb b/spec/migrations/clean_upload_symlinks_spec.rb
index cecb3ddac53..26653b9c008 100644
--- a/spec/migrations/clean_upload_symlinks_spec.rb
+++ b/spec/migrations/clean_upload_symlinks_spec.rb
@@ -5,7 +5,7 @@ describe CleanUploadSymlinks do
let(:migration) { described_class.new }
let(:test_dir) { File.join(Rails.root, "tmp", "tests", "move_uploads_test") }
let(:uploads_dir) { File.join(test_dir, "public", "uploads") }
- let(:new_uploads_dir) { File.join(uploads_dir, "system") }
+ let(:new_uploads_dir) { File.join(uploads_dir, "-", "system") }
let(:original_path) { File.join(new_uploads_dir, 'user') }
let(:symlink_path) { File.join(uploads_dir, 'user') }
diff --git a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb
index 12cac1d033d..b47f3314926 100644
--- a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb
+++ b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb
@@ -4,7 +4,7 @@ require Rails.root.join('db', 'post_migrate', '20170502101023_cleanup_namespacel
describe CleanupNamespacelessPendingDeleteProjects do
before do
# Stub after_save callbacks that will fail when Project has no namespace
- allow_any_instance_of(Project).to receive(:ensure_storage_path_exist).and_return(nil)
+ allow_any_instance_of(Project).to receive(:ensure_storage_path_exists).and_return(nil)
allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil)
end
diff --git a/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb b/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb
new file mode 100644
index 00000000000..7879105a334
--- /dev/null
+++ b/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170816102555_cleanup_nonexisting_namespace_pending_delete_projects.rb')
+
+describe CleanupNonexistingNamespacePendingDeleteProjects do
+ before do
+ # Stub after_save callbacks that will fail when Project has invalid namespace
+ allow_any_instance_of(Project).to receive(:ensure_storage_path_exist).and_return(nil)
+ allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil)
+ end
+
+ describe '#up' do
+ set(:some_project) { create(:project) }
+
+ it 'only cleans up when namespace does not exist' do
+ create(:project, pending_delete: true)
+ project = build(:project, pending_delete: true, namespace: nil, namespace_id: Namespace.maximum(:id).to_i.succ)
+ project.save(validate: false)
+
+ expect(NamespacelessProjectDestroyWorker).to receive(:bulk_perform_async).with([[project.id]])
+
+ described_class.new.up
+ end
+
+ it 'does nothing when no pending delete projects without namespace found' do
+ create(:project, pending_delete: true, namespace: create(:namespace))
+
+ expect(NamespacelessProjectDestroyWorker).not_to receive(:bulk_perform_async)
+
+ described_class.new.up
+ end
+ end
+end
diff --git a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb
index 1396d12e5a9..759e77ac9db 100644
--- a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb
+++ b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170607121233_convert_custom_notification_settings_to_columns')
describe ConvertCustomNotificationSettingsToColumns, :migration do
+ let(:user_class) { table(:users) }
+
let(:settings_params) do
[
{ level: 0, events: [:new_note] }, # disabled, single event
@@ -19,7 +21,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do
events[event] = true
end
- user = create(:user)
+ user = build(:user).becomes(user_class).tap(&:save!)
create_params = { user_id: user.id, level: params[:level], events: events }
notification_setting = described_class::NotificationSetting.create(create_params)
@@ -35,7 +37,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do
events[event] = true
end
- user = create(:user)
+ user = build(:user).becomes(user_class).tap(&:save!)
create_params = events.merge(user_id: user.id, level: params[:level])
notification_setting = described_class::NotificationSetting.create(create_params)
diff --git a/spec/migrations/migrate_issues_to_ghost_user_spec.rb b/spec/migrations/migrate_issues_to_ghost_user_spec.rb
new file mode 100644
index 00000000000..cfd4021fbac
--- /dev/null
+++ b/spec/migrations/migrate_issues_to_ghost_user_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20170825104051_migrate_issues_to_ghost_user.rb')
+
+describe MigrateIssuesToGhostUser, :migration do
+ describe '#up' do
+ let(:projects) { table(:projects) }
+ let(:issues) { table(:issues) }
+ let(:users) { table(:users) }
+
+ before do
+ projects.create!(name: 'gitlab')
+ user = users.create(email: 'test@example.com')
+ issues.create(title: 'Issue 1', author_id: nil, project_id: 1)
+ issues.create(title: 'Issue 2', author_id: user.id, project_id: 1)
+ end
+
+ context 'when ghost user exists' do
+ let!(:ghost) { users.create(ghost: true, email: 'ghost@example.com') }
+
+ it 'does not create a new user' do
+ expect { schema_migrate_up! }.not_to change { User.count }
+ end
+
+ it 'migrates issues where author = nil to the ghost user' do
+ schema_migrate_up!
+
+ expect(issues.first.reload.author_id).to eq(ghost.id)
+ end
+
+ it 'does not change issues authored by an existing user' do
+ expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id}
+ end
+ end
+
+ context 'when ghost user does not exist' do
+ it 'creates a new user' do
+ expect { schema_migrate_up! }.to change { User.count }.by(1)
+ end
+
+ it 'migrates issues where author = nil to the ghost user' do
+ schema_migrate_up!
+
+ expect(issues.first.reload.author_id).to eq(User.ghost.id)
+ end
+
+ it 'does not change issues authored by an existing user' do
+ expect { schema_migrate_up! }.not_to change { issues.second.reload.author_id}
+ end
+ end
+ end
+end
diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb
index cfe1ca481b2..81366d15b34 100644
--- a/spec/migrations/migrate_old_artifacts_spec.rb
+++ b/spec/migrations/migrate_old_artifacts_spec.rb
@@ -10,7 +10,7 @@ describe MigrateOldArtifacts do
before do
allow(Gitlab.config.artifacts).to receive(:path).and_return(directory)
end
-
+
after do
FileUtils.remove_entry_secure(directory)
end
@@ -95,7 +95,7 @@ describe MigrateOldArtifacts do
FileUtils.copy(
Rails.root.join('spec/fixtures/ci_build_artifacts.zip'),
File.join(legacy_path(build), "ci_build_artifacts.zip"))
-
+
FileUtils.copy(
Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'),
File.join(legacy_path(build), "ci_build_artifacts_metadata.gz"))
diff --git a/spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb b/spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb
new file mode 100644
index 00000000000..e02bcd2f4da
--- /dev/null
+++ b/spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170822101017_migrate_pipeline_sidekiq_queues.rb')
+
+describe MigratePipelineSidekiqQueues, :sidekiq, :redis do
+ include Gitlab::Database::MigrationHelpers
+
+ context 'when there are jobs in the queues' do
+ it 'correctly migrates queue when migrating up' do
+ Sidekiq::Testing.disable! do
+ stubbed_worker(queue: :pipeline).perform_async('Something', [1])
+ stubbed_worker(queue: :build).perform_async('Something', [1])
+
+ described_class.new.up
+
+ expect(sidekiq_queue_length('pipeline')).to eq 0
+ expect(sidekiq_queue_length('build')).to eq 0
+ expect(sidekiq_queue_length('pipeline_default')).to eq 2
+ end
+ end
+
+ it 'correctly migrates queue when migrating down' do
+ Sidekiq::Testing.disable! do
+ stubbed_worker(queue: :pipeline_default).perform_async('Class', [1])
+ stubbed_worker(queue: :pipeline_processing).perform_async('Class', [2])
+ stubbed_worker(queue: :pipeline_hooks).perform_async('Class', [3])
+ stubbed_worker(queue: :pipeline_cache).perform_async('Class', [4])
+
+ described_class.new.down
+
+ expect(sidekiq_queue_length('pipeline')).to eq 4
+ expect(sidekiq_queue_length('pipeline_default')).to eq 0
+ expect(sidekiq_queue_length('pipeline_processing')).to eq 0
+ expect(sidekiq_queue_length('pipeline_hooks')).to eq 0
+ expect(sidekiq_queue_length('pipeline_cache')).to eq 0
+ end
+ end
+ end
+
+ context 'when there are no jobs in the queues' do
+ it 'does not raise error when migrating up' do
+ expect { described_class.new.up }.not_to raise_error
+ end
+
+ it 'does not raise error when migrating down' do
+ expect { described_class.new.down }.not_to raise_error
+ end
+ end
+
+ def stubbed_worker(queue:)
+ Class.new do
+ include Sidekiq::Worker
+ sidekiq_options queue: queue
+ end
+ end
+end
diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
index cf2d5827306..e5793a3c0ee 100644
--- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
+++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
@@ -6,7 +6,7 @@ require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_
describe MigrateProcessCommitWorkerJobs do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:commit) { project.commit.raw.raw_commit }
+ let(:commit) { project.commit.raw.rugged_commit }
describe 'Project' do
describe 'find_including_path' do
diff --git a/spec/migrations/migrate_stage_id_reference_in_background_spec.rb b/spec/migrations/migrate_stage_id_reference_in_background_spec.rb
index 260378adaa7..9b92f4b70b0 100644
--- a/spec/migrations/migrate_stage_id_reference_in_background_spec.rb
+++ b/spec/migrations/migrate_stage_id_reference_in_background_spec.rb
@@ -2,19 +2,6 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170628080858_migrate_stage_id_reference_in_background')
describe MigrateStageIdReferenceInBackground, :migration, :sidekiq do
- matcher :be_scheduled_migration do |delay, *expected|
- match do |migration|
- BackgroundMigrationWorker.jobs.any? do |job|
- job['args'] == [migration, expected] &&
- job['at'].to_i == (delay.to_i + Time.now.to_i)
- end
- end
-
- failure_message do |migration|
- "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
- end
- end
-
let(:jobs) { table(:ci_builds) }
let(:stages) { table(:ci_stages) }
let(:pipelines) { table(:ci_pipelines) }
diff --git a/spec/migrations/migrate_stages_statuses_spec.rb b/spec/migrations/migrate_stages_statuses_spec.rb
new file mode 100644
index 00000000000..094c9bc604e
--- /dev/null
+++ b/spec/migrations/migrate_stages_statuses_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170711145558_migrate_stages_statuses.rb')
+
+describe MigrateStagesStatuses, :migration do
+ let(:jobs) { table(:ci_builds) }
+ let(:stages) { table(:ci_stages) }
+ let(:pipelines) { table(:ci_pipelines) }
+ let(:projects) { table(:projects) }
+
+ STATUSES = { created: 0, pending: 1, running: 2, success: 3,
+ failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ stub_const("#{described_class.name}::RANGE_SIZE", 1)
+
+ projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1')
+ projects.create!(id: 2, name: 'gitlab2', path: 'gitlab2')
+
+ pipelines.create!(id: 1, project_id: 1, ref: 'master', sha: 'adf43c3a')
+ pipelines.create!(id: 2, project_id: 2, ref: 'feature', sha: '21a3deb')
+
+ create_job(project: 1, pipeline: 1, stage: 'test', status: 'success')
+ create_job(project: 1, pipeline: 1, stage: 'test', status: 'running')
+ create_job(project: 1, pipeline: 1, stage: 'build', status: 'success')
+ create_job(project: 1, pipeline: 1, stage: 'build', status: 'failed')
+ create_job(project: 2, pipeline: 2, stage: 'test', status: 'success')
+ create_job(project: 2, pipeline: 2, stage: 'test', status: 'success')
+ create_job(project: 2, pipeline: 2, stage: 'test', status: 'failed', retried: true)
+
+ stages.create!(id: 1, pipeline_id: 1, project_id: 1, name: 'test', status: nil)
+ stages.create!(id: 2, pipeline_id: 1, project_id: 1, name: 'build', status: nil)
+ stages.create!(id: 3, pipeline_id: 2, project_id: 2, name: 'test', status: nil)
+ end
+
+ it 'correctly migrates stages statuses' do
+ Sidekiq::Testing.inline! do
+ expect(stages.where(status: nil).count).to eq 3
+
+ migrate!
+
+ expect(stages.where(status: nil)).to be_empty
+ expect(stages.all.order('id ASC').pluck(:status))
+ .to eq [STATUSES[:running], STATUSES[:failed], STATUSES[:success]]
+ end
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1)
+ expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 2, 2)
+ expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 3, 3)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+
+ def create_job(project:, pipeline:, stage:, status:, **opts)
+ stages = { test: 1, build: 2, deploy: 3 }
+
+ jobs.create!(project_id: project, commit_id: pipeline,
+ stage_idx: stages[stage.to_sym], stage: stage,
+ status: status, **opts)
+ end
+end
diff --git a/spec/migrations/move_personal_snippets_files_spec.rb b/spec/migrations/move_personal_snippets_files_spec.rb
index 8505c7bf3e3..1a319eccc0d 100644
--- a/spec/migrations/move_personal_snippets_files_spec.rb
+++ b/spec/migrations/move_personal_snippets_files_spec.rb
@@ -5,7 +5,7 @@ describe MovePersonalSnippetsFiles do
let(:migration) { described_class.new }
let(:test_dir) { File.join(Rails.root, "tmp", "tests", "move_snippet_files_test") }
let(:uploads_dir) { File.join(test_dir, 'uploads') }
- let(:new_uploads_dir) { File.join(uploads_dir, 'system') }
+ let(:new_uploads_dir) { File.join(uploads_dir, '-', 'system') }
before do
allow(CarrierWave).to receive(:root).and_return(test_dir)
@@ -42,7 +42,7 @@ describe MovePersonalSnippetsFiles do
describe 'updating the markdown' do
it 'includes the new path when the file exists' do
secret = "secret#{snippet.id}"
- file_location = "/uploads/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg"
+ file_location = "/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg"
migration.up
@@ -60,7 +60,7 @@ describe MovePersonalSnippetsFiles do
it 'updates the note markdown' do
secret = "secret#{snippet.id}"
- file_location = "/uploads/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg"
+ file_location = "/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg"
markdown = markdown_linking_file('picture.jpg', snippet)
note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}")
@@ -108,7 +108,7 @@ describe MovePersonalSnippetsFiles do
it 'keeps the markdown as is when the file is missing' do
secret = "secret#{snippet_with_missing_file.id}"
- file_location = "/uploads/system/personal_snippet/#{snippet_with_missing_file.id}/#{secret}/picture.jpg"
+ file_location = "/uploads/-/system/personal_snippet/#{snippet_with_missing_file.id}/#{secret}/picture.jpg"
migration.down
@@ -167,7 +167,7 @@ describe MovePersonalSnippetsFiles do
def markdown_linking_file(filename, snippet, in_new_path: false)
markdown = "![#{filename.split('.')[0]}]"
markdown += '(/uploads'
- markdown += '/system' if in_new_path
+ markdown += '/-/system' if in_new_path
markdown += "/#{model_file_path(filename, snippet)})"
markdown
end
diff --git a/spec/migrations/move_system_upload_folder_spec.rb b/spec/migrations/move_system_upload_folder_spec.rb
index b622b4e9536..d3180477db3 100644
--- a/spec/migrations/move_system_upload_folder_spec.rb
+++ b/spec/migrations/move_system_upload_folder_spec.rb
@@ -33,6 +33,15 @@ describe MoveSystemUploadFolder do
expect(File.symlink?(File.join(test_base, 'system'))).to be_truthy
expect(File.exist?(File.join(test_base, 'system', 'file'))).to be_truthy
end
+
+ it 'does not move if the target directory already exists' do
+ FileUtils.mkdir_p(File.join(test_base, '-', 'system'))
+
+ expect(FileUtils).not_to receive(:mv)
+ expect(migration).to receive(:say).with(/already exists. No need to redo the move/)
+
+ migration.up
+ end
end
describe '#down' do
@@ -58,5 +67,14 @@ describe MoveSystemUploadFolder do
expect(File.directory?(File.join(test_base, 'system'))).to be_truthy
expect(File.symlink?(File.join(test_base, 'system'))).to be_falsey
end
+
+ it 'does not move if the old directory already exists' do
+ FileUtils.mkdir_p(File.join(test_base, 'system'))
+
+ expect(FileUtils).not_to receive(:mv)
+ expect(migration).to receive(:say).with(/already exists and is not a symlink, no need to revert/)
+
+ migration.down
+ end
end
end
diff --git a/spec/migrations/move_uploads_to_system_dir_spec.rb b/spec/migrations/move_uploads_to_system_dir_spec.rb
index 37d66452447..ca11a2004c5 100644
--- a/spec/migrations/move_uploads_to_system_dir_spec.rb
+++ b/spec/migrations/move_uploads_to_system_dir_spec.rb
@@ -5,7 +5,7 @@ describe MoveUploadsToSystemDir do
let(:migration) { described_class.new }
let(:test_dir) { File.join(Rails.root, "tmp", "move_uploads_test") }
let(:uploads_dir) { File.join(test_dir, "public", "uploads") }
- let(:new_uploads_dir) { File.join(uploads_dir, "system") }
+ let(:new_uploads_dir) { File.join(uploads_dir, "-", "system") }
before do
FileUtils.remove_dir(test_dir) if File.directory?(test_dir)
diff --git a/spec/migrations/remove_dot_git_from_usernames_spec.rb b/spec/migrations/remove_dot_git_from_usernames_spec.rb
index 8737e00eaeb..129374cb38c 100644
--- a/spec/migrations/remove_dot_git_from_usernames_spec.rb
+++ b/spec/migrations/remove_dot_git_from_usernames_spec.rb
@@ -51,7 +51,6 @@ describe RemoveDotGitFromUsernames do
namespace.path = path
namespace.save!(validate: false)
- user.username = path
- user.save!(validate: false)
+ user.update_column(:username, path)
end
end
diff --git a/spec/migrations/remove_duplicate_mr_events_spec.rb b/spec/migrations/remove_duplicate_mr_events_spec.rb
new file mode 100644
index 00000000000..e393374028f
--- /dev/null
+++ b/spec/migrations/remove_duplicate_mr_events_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170815060945_remove_duplicate_mr_events.rb')
+
+describe RemoveDuplicateMrEvents, truncate: true do
+ let(:migration) { described_class.new }
+
+ describe '#up' do
+ let(:user) { create(:user) }
+ let(:merge_requests) { create_list(:merge_request, 2) }
+ let(:issue) { create(:issue) }
+ let!(:events) do
+ [
+ create(:event, :created, author: user, target: merge_requests.first),
+ create(:event, :created, author: user, target: merge_requests.first),
+ create(:event, :updated, author: user, target: merge_requests.first),
+ create(:event, :created, author: user, target: merge_requests.second),
+ create(:event, :created, author: user, target: issue),
+ create(:event, :created, author: user, target: issue)
+ ]
+ end
+
+ it 'removes duplicated merge request create records' do
+ expect { migration.up }.to change { Event.count }.from(6).to(5)
+ end
+ end
+end
diff --git a/spec/migrations/rename_system_namespaces_spec.rb b/spec/migrations/rename_system_namespaces_spec.rb
deleted file mode 100644
index 747694cbe33..00000000000
--- a/spec/migrations/rename_system_namespaces_spec.rb
+++ /dev/null
@@ -1,254 +0,0 @@
-require "spec_helper"
-require Rails.root.join("db", "migrate", "20170316163800_rename_system_namespaces.rb")
-
-describe RenameSystemNamespaces, truncate: true do
- let(:migration) { described_class.new }
- let(:test_dir) { File.join(Rails.root, "tmp", "tests", "rename_namespaces_test") }
- let(:uploads_dir) { File.join(test_dir, "public", "uploads") }
- let(:system_namespace) do
- namespace = build(:namespace, path: "system")
- namespace.save(validate: false)
- namespace
- end
-
- def save_invalid_routable(routable)
- routable.__send__(:prepare_route)
- routable.save(validate: false)
- end
-
- before do
- FileUtils.remove_dir(test_dir) if File.directory?(test_dir)
- FileUtils.mkdir_p(uploads_dir)
- FileUtils.remove_dir(TestEnv.repos_path) if File.directory?(TestEnv.repos_path)
- allow(migration).to receive(:say)
- allow(migration).to receive(:uploads_dir).and_return(uploads_dir)
- end
-
- describe "#system_namespace" do
- it "only root namespaces called with path `system`" do
- system_namespace
- system_namespace_with_parent = build(:namespace, path: 'system', parent: create(:namespace))
- system_namespace_with_parent.save(validate: false)
-
- expect(migration.system_namespace.id).to eq(system_namespace.id)
- end
- end
-
- describe "#up" do
- before do
- system_namespace
- end
-
- it "doesn't break if there are no namespaces called system" do
- Namespace.delete_all
-
- migration.up
- end
-
- it "renames namespaces called system" do
- migration.up
-
- expect(system_namespace.reload.path).to eq("system0")
- end
-
- it "renames the route to the namespace" do
- migration.up
-
- expect(system_namespace.reload.full_path).to eq("system0")
- end
-
- it "renames the route for projects of the namespace" do
- project = build(:project, :repository, path: "project-path", namespace: system_namespace)
- save_invalid_routable(project)
-
- migration.up
-
- expect(project.route.reload.path).to eq("system0/project-path")
- end
-
- it "doesn't touch routes of namespaces that look like system" do
- namespace = create(:group, path: 'systemlookalike')
- project = create(:project, :repository, namespace: namespace, path: 'the-project')
-
- migration.up
-
- expect(project.route.reload.path).to eq('systemlookalike/the-project')
- expect(namespace.route.reload.path).to eq('systemlookalike')
- end
-
- it "moves the the repository for a project in the namespace" do
- project = build(:project, :repository, namespace: system_namespace, path: "system-project")
- save_invalid_routable(project)
- TestEnv.copy_repo(project,
- bare_repo: TestEnv.factory_repo_path_bare,
- refs: TestEnv::BRANCH_SHA)
- expected_repo = File.join(TestEnv.repos_path, "system0", "system-project.git")
-
- migration.up
-
- expect(File.directory?(expected_repo)).to be(true)
- end
-
- it "moves the uploads for the namespace" do
- allow(migration).to receive(:move_namespace_folders).with(Settings.pages.path, "system", "system0")
- expect(migration).to receive(:move_namespace_folders).with(uploads_dir, "system", "system0")
-
- migration.up
- end
-
- it "moves the pages for the namespace" do
- allow(migration).to receive(:move_namespace_folders).with(uploads_dir, "system", "system0")
- expect(migration).to receive(:move_namespace_folders).with(Settings.pages.path, "system", "system0")
-
- migration.up
- end
-
- describe "clears the markdown cache for projects in the system namespace" do
- let!(:project) do
- project = build(:project, :repository, namespace: system_namespace)
- save_invalid_routable(project)
- project
- end
-
- it 'removes description_html from projects' do
- migration.up
-
- expect(project.reload.description_html).to be_nil
- end
-
- it 'removes issue descriptions' do
- issue = create(:issue, project: project, description_html: 'Issue description')
-
- migration.up
-
- expect(issue.reload.description_html).to be_nil
- end
-
- it 'removes merge request descriptions' do
- merge_request = create(:merge_request,
- source_project: project,
- target_project: project,
- description_html: 'MergeRequest description')
-
- migration.up
-
- expect(merge_request.reload.description_html).to be_nil
- end
-
- it 'removes note html' do
- note = create(:note,
- project: project,
- noteable: create(:issue, project: project),
- note_html: 'note description')
-
- migration.up
-
- expect(note.reload.note_html).to be_nil
- end
-
- it 'removes milestone description' do
- milestone = create(:milestone,
- project: project,
- description_html: 'milestone description')
-
- migration.up
-
- expect(milestone.reload.description_html).to be_nil
- end
- end
-
- context "system namespace -> subgroup -> system0 project" do
- it "updates the route of the project correctly" do
- subgroup = build(:group, path: "subgroup", parent: system_namespace)
- save_invalid_routable(subgroup)
- project = build(:project, :repository, path: "system0", namespace: subgroup)
- save_invalid_routable(project)
-
- migration.up
-
- expect(project.route.reload.path).to eq("system0/subgroup/system0")
- end
- end
- end
-
- describe "#move_repositories" do
- let(:namespace) { create(:group, name: "hello-group") }
- it "moves a project for a namespace" do
- create(:project, :repository, namespace: namespace, path: "hello-project")
- expected_path = File.join(TestEnv.repos_path, "bye-group", "hello-project.git")
-
- migration.move_repositories(namespace, "hello-group", "bye-group")
-
- expect(File.directory?(expected_path)).to be(true)
- end
-
- it "moves a namespace in a subdirectory correctly" do
- child_namespace = create(:group, name: "sub-group", parent: namespace)
- create(:project, :repository, namespace: child_namespace, path: "hello-project")
-
- expected_path = File.join(TestEnv.repos_path, "hello-group", "renamed-sub-group", "hello-project.git")
-
- migration.move_repositories(child_namespace, "hello-group/sub-group", "hello-group/renamed-sub-group")
-
- expect(File.directory?(expected_path)).to be(true)
- end
-
- it "moves a parent namespace with subdirectories" do
- child_namespace = create(:group, name: "sub-group", parent: namespace)
- create(:project, :repository, namespace: child_namespace, path: "hello-project")
- expected_path = File.join(TestEnv.repos_path, "renamed-group", "sub-group", "hello-project.git")
-
- migration.move_repositories(child_namespace, "hello-group", "renamed-group")
-
- expect(File.directory?(expected_path)).to be(true)
- end
- end
-
- describe "#move_namespace_folders" do
- it "moves a namespace with files" do
- source = File.join(uploads_dir, "parent-group", "sub-group")
- FileUtils.mkdir_p(source)
- destination = File.join(uploads_dir, "parent-group", "moved-group")
- FileUtils.touch(File.join(source, "test.txt"))
- expected_file = File.join(destination, "test.txt")
-
- migration.move_namespace_folders(uploads_dir, File.join("parent-group", "sub-group"), File.join("parent-group", "moved-group"))
-
- expect(File.exist?(expected_file)).to be(true)
- end
-
- it "moves a parent namespace uploads" do
- source = File.join(uploads_dir, "parent-group", "sub-group")
- FileUtils.mkdir_p(source)
- destination = File.join(uploads_dir, "moved-parent", "sub-group")
- FileUtils.touch(File.join(source, "test.txt"))
- expected_file = File.join(destination, "test.txt")
-
- migration.move_namespace_folders(uploads_dir, "parent-group", "moved-parent")
-
- expect(File.exist?(expected_file)).to be(true)
- end
- end
-
- describe "#child_ids_for_parent" do
- it "collects child ids for all levels" do
- parent = create(:group)
- first_child = create(:group, parent: parent)
- second_child = create(:group, parent: parent)
- third_child = create(:group, parent: second_child)
- all_ids = [parent.id, first_child.id, second_child.id, third_child.id]
-
- collected_ids = migration.child_ids_for_parent(parent, ids: [parent.id])
-
- expect(collected_ids).to contain_exactly(*all_ids)
- end
- end
-
- describe "#remove_last_ocurrence" do
- it "removes only the last occurance of a string" do
- input = "this/is/system/namespace/with/system"
-
- expect(migration.remove_last_occurrence(input, "system")).to eq("this/is/system/namespace/with/")
- end
- end
-end
diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb
index 11412005b72..0a45c5ea32d 100644
--- a/spec/migrations/update_upload_paths_to_system_spec.rb
+++ b/spec/migrations/update_upload_paths_to_system_spec.rb
@@ -11,7 +11,7 @@ describe UpdateUploadPathsToSystem do
describe "#uploads_to_switch_to_new_path" do
it "contains only uploads with the old path for the correct models" do
_upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg")
- _upload_with_system_path = create(:upload, model: create(:project), path: "uploads/system/project/avatar.jpg")
+ _upload_with_system_path = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg")
_upload_with_other_path = create(:upload, model: create(:project), path: "thelongsecretforafileupload/avatar.jpg")
old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg")
group_upload = create(:upload, model: create(:group), path: "uploads/group/avatar.jpg")
@@ -23,7 +23,7 @@ describe UpdateUploadPathsToSystem do
describe "#uploads_to_switch_to_old_path" do
it "contains only uploads with the new path for the correct models" do
_upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg")
- upload_with_system_path = create(:upload, model: create(:project), path: "uploads/system/project/avatar.jpg")
+ upload_with_system_path = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg")
_upload_with_other_path = create(:upload, model: create(:project), path: "thelongsecretforafileupload/avatar.jpg")
_old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg")
@@ -37,13 +37,13 @@ describe UpdateUploadPathsToSystem do
migration.up
- expect(old_upload.reload.path).to eq("uploads/system/project/avatar.jpg")
+ expect(old_upload.reload.path).to eq("uploads/-/system/project/avatar.jpg")
end
end
describe "#down", truncate: true 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")
+ new_upload = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg")
migration.down
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 7cd3a84d592..b5d5d58697b 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -9,4 +9,39 @@ RSpec.describe Appearance do
it { is_expected.to validate_presence_of(:description) }
it { is_expected.to have_many(:uploads).dependent(:destroy) }
+
+ describe '.current', :use_clean_rails_memory_store_caching do
+ let!(:appearance) { create(:appearance) }
+
+ it 'returns the current appearance row' do
+ expect(described_class.current).to eq(appearance)
+ end
+
+ it 'caches the result' do
+ expect(described_class).to receive(:first).once
+
+ 2.times { described_class.current }
+ end
+ end
+
+ describe '#flush_redis_cache' do
+ it 'flushes the cache in Redis' do
+ appearance = create(:appearance)
+
+ expect(Rails.cache).to receive(:delete).with(described_class::CACHE_KEY)
+
+ appearance.flush_redis_cache
+ end
+ end
+
+ describe '#single_appearance_row' do
+ it 'adds an error when more than 1 row exists' do
+ create(:appearance)
+
+ new_row = build(:appearance)
+ new_row.save
+
+ expect(new_row.valid?).to eq(false)
+ end
+ end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 359753b600e..c7a9eabdf06 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -5,6 +5,7 @@ describe ApplicationSetting do
it { expect(setting).to be_valid }
it { expect(setting.uuid).to be_present }
+ it { expect(setting).to have_db_column(:auto_devops_enabled) }
describe 'validations' do
let(:http) { 'http://example.com' }
@@ -72,6 +73,33 @@ describe ApplicationSetting do
.is_greater_than(0)
end
+ context 'key restrictions' do
+ it 'supports all key types' do
+ expect(described_class::SUPPORTED_KEY_TYPES).to contain_exactly(:rsa, :dsa, :ecdsa, :ed25519)
+ end
+
+ it 'does not allow all key types to be disabled' do
+ described_class::SUPPORTED_KEY_TYPES.each do |type|
+ setting["#{type}_key_restriction"] = described_class::FORBIDDEN_KEY_VALUE
+ end
+
+ expect(setting).not_to be_valid
+ expect(setting.errors.messages).to have_key(:allowed_key_types)
+ end
+
+ where(:type) do
+ described_class::SUPPORTED_KEY_TYPES
+ end
+
+ with_them do
+ let(:field) { :"#{type}_key_restriction" }
+
+ it { is_expected.to validate_presence_of(field) }
+ it { is_expected.to allow_value(*KeyRestrictionValidator.supported_key_restrictions(type)).for(field) }
+ it { is_expected.not_to allow_value(128).for(field) }
+ end
+ end
+
it_behaves_like 'an object with email-formated attributes', :admin_notification_email do
subject { setting }
end
@@ -441,4 +469,36 @@ describe ApplicationSetting do
end
end
end
+
+ describe '#allowed_key_types' do
+ it 'includes all key types by default' do
+ expect(setting.allowed_key_types).to contain_exactly(*described_class::SUPPORTED_KEY_TYPES)
+ end
+
+ it 'excludes disabled key types' do
+ expect(setting.allowed_key_types).to include(:ed25519)
+
+ setting.ed25519_key_restriction = described_class::FORBIDDEN_KEY_VALUE
+
+ expect(setting.allowed_key_types).not_to include(:ed25519)
+ end
+ end
+
+ describe '#key_restriction_for' do
+ it 'returns the restriction value for recognised types' do
+ setting.rsa_key_restriction = 1024
+
+ expect(setting.key_restriction_for(:rsa)).to eq(1024)
+ end
+
+ it 'allows types to be passed as a string' do
+ setting.rsa_key_restriction = 1024
+
+ expect(setting.key_restriction_for('rsa')).to eq(1024)
+ end
+
+ it 'returns forbidden for unrecognised type' do
+ expect(setting.key_restriction_for(:foo)).to eq(described_class::FORBIDDEN_KEY_VALUE)
+ end
+ end
end
diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb
index 87e60d9c16b..b909e04dfc3 100644
--- a/spec/models/award_emoji_spec.rb
+++ b/spec/models/award_emoji_spec.rb
@@ -41,4 +41,40 @@ describe AwardEmoji do
end
end
end
+
+ describe 'expiring ETag cache' do
+ context 'on a note' do
+ let(:note) { create(:note_on_issue) }
+ let(:award_emoji) { build(:award_emoji, user: build(:user), awardable: note) }
+
+ it 'calls expire_etag_cache on the note when saved' do
+ expect(note).to receive(:expire_etag_cache)
+
+ award_emoji.save!
+ end
+
+ it 'calls expire_etag_cache on the note when destroyed' do
+ expect(note).to receive(:expire_etag_cache)
+
+ award_emoji.destroy!
+ end
+ end
+
+ context 'on another awardable' do
+ let(:issue) { create(:issue) }
+ let(:award_emoji) { build(:award_emoji, user: build(:user), awardable: issue) }
+
+ it 'does not call expire_etag_cache on the issue when saved' do
+ expect(issue).not_to receive(:expire_etag_cache)
+
+ award_emoji.save!
+ end
+
+ it 'does not call expire_etag_cache on the issue when destroyed' do
+ expect(issue).not_to receive(:expire_etag_cache)
+
+ award_emoji.destroy!
+ end
+ end
+ end
end
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index a8ca1d110e4..461e754dc1f 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -20,7 +20,7 @@ describe BroadcastMessage do
it { is_expected.not_to allow_value('000').for(:font) }
end
- describe '.current' do
+ describe '.current', :use_clean_rails_memory_store_caching do
it 'returns message if time match' do
message = create(:broadcast_message)
@@ -45,6 +45,37 @@ describe BroadcastMessage do
expect(described_class.current).to be_empty
end
+
+ it 'caches the output of the query' do
+ create(:broadcast_message)
+
+ expect(described_class).to receive(:where).and_call_original.once
+
+ 2.times { described_class.current }
+ end
+
+ it 'includes messages that need to be displayed in the future' do
+ create(:broadcast_message)
+
+ future = create(
+ :broadcast_message,
+ starts_at: Time.now + 10.minutes,
+ ends_at: Time.now + 20.minutes
+ )
+
+ expect(described_class.current.length).to eq(1)
+
+ Timecop.travel(future.starts_at) do
+ expect(described_class.current.length).to eq(2)
+ end
+ end
+
+ it 'does not clear the cache if only a future message should be displayed' do
+ create(:broadcast_message, :future)
+
+ expect(Rails.cache).not_to receive(:delete)
+ expect(described_class.current.length).to eq(0)
+ end
end
describe '#active?' do
@@ -102,4 +133,14 @@ describe BroadcastMessage do
end
end
end
+
+ describe '#flush_redis_cache' do
+ it 'flushes the Redis cache' do
+ message = create(:broadcast_message)
+
+ expect(Rails.cache).to receive(:delete).with(described_class::CACHE_KEY)
+
+ message.flush_redis_cache
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 46523b8b5a8..d67f578ab8a 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -21,6 +21,16 @@ describe Ci::Build do
it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) }
+ describe 'callbacks' do
+ context 'when running after_create callback' do
+ it 'triggers asynchronous build hooks worker' do
+ expect(BuildHooksWorker).to receive(:perform_async)
+
+ create(:ci_build)
+ end
+ end
+ end
+
describe '.manual_actions' do
let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) }
let!(:manual_but_succeeded) { create(:ci_build, :manual, status: :success, pipeline: pipeline) }
@@ -33,6 +43,32 @@ describe Ci::Build do
it { is_expected.not_to include(manual_but_created) }
end
+ describe '.ref_protected' do
+ subject { described_class.ref_protected }
+
+ context 'when protected is true' do
+ let!(:job) { create(:ci_build, :protected) }
+
+ it { is_expected.to include(job) }
+ end
+
+ context 'when protected is false' do
+ let!(:job) { create(:ci_build) }
+
+ it { is_expected.not_to include(job) }
+ end
+
+ context 'when protected is nil' do
+ let!(:job) { create(:ci_build) }
+
+ before do
+ job.update_attribute(:protected, nil)
+ end
+
+ it { is_expected.not_to include(job) }
+ end
+ end
+
describe '#actionize' do
context 'when build is a created' do
before do
@@ -1264,7 +1300,7 @@ describe Ci::Build do
{ key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
{ key: 'CI_PROJECT_NAME', value: project.path, public: true },
{ key: 'CI_PROJECT_PATH', value: project.full_path, public: true },
- { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path.parameterize, public: true },
+ { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true },
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true },
{ key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
@@ -1291,8 +1327,12 @@ describe Ci::Build do
context 'when build has user' do
let(:user_variables) do
- [{ key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
- { key: 'GITLAB_USER_EMAIL', value: user.email, public: true }]
+ [
+ { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
+ { key: 'GITLAB_USER_EMAIL', value: user.email, public: true },
+ { key: 'GITLAB_USER_LOGIN', value: user.username, public: true },
+ { key: 'GITLAB_USER_NAME', value: user.name, public: true }
+ ]
end
before do
@@ -1496,10 +1536,12 @@ describe Ci::Build do
context 'when build is for triggers' do
let(:trigger) { create(:ci_trigger, project: project) }
- let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) }
+ let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) }
+
let(:user_trigger_variable) do
- { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false }
+ { key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1', public: false }
end
+
let(:predefined_trigger_variable) do
{ key: 'CI_PIPELINE_TRIGGERED', value: 'true', public: true }
end
@@ -1508,8 +1550,26 @@ describe Ci::Build do
build.trigger_request = trigger_request
end
- it { is_expected.to include(user_trigger_variable) }
- it { is_expected.to include(predefined_trigger_variable) }
+ shared_examples 'returns variables for triggers' do
+ it { is_expected.to include(user_trigger_variable) }
+ it { is_expected.to include(predefined_trigger_variable) }
+ end
+
+ context 'when variables are stored in trigger_request' do
+ before do
+ trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
+ end
+
+ it_behaves_like 'returns variables for triggers'
+ end
+
+ context 'when variables are stored in pipeline_variables' do
+ before do
+ create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1')
+ end
+
+ it_behaves_like 'returns variables for triggers'
+ end
end
context 'when pipeline has a variable' do
@@ -1672,6 +1732,30 @@ describe Ci::Build do
{ key: 'secret', value: 'value', public: false }])
end
end
+
+ context 'when using auto devops' do
+ context 'and is enabled' do
+ before do
+ project.create_auto_devops!(enabled: true, domain: 'example.com')
+ end
+
+ it "includes AUTO_DEVOPS_DOMAIN" do
+ is_expected.to include(
+ { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true })
+ end
+ end
+
+ context 'and is disabled' do
+ before do
+ project.create_auto_devops!(enabled: false, domain: 'example.com')
+ end
+
+ it "includes AUTO_DEVOPS_DOMAIN" do
+ is_expected.not_to include(
+ { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true })
+ end
+ end
+ end
end
describe 'state transition: any => [:pending]' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index ac75c6501ee..95da97b7bc5 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Ci::Pipeline, :mailer do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ set(:project) { create(:project) }
let(:pipeline) do
create(:ci_empty_pipeline, status: :created, project: project)
@@ -159,6 +159,18 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#predefined_variables' do
+ subject { pipeline.predefined_variables }
+
+ it { is_expected.to be_an(Array) }
+
+ it 'includes the defined keys' do
+ keys = subject.map { |v| v[:key] }
+
+ expect(keys).to include('CI_PIPELINE_ID', 'CI_CONFIG_PATH', 'CI_PIPELINE_SOURCE')
+ end
+ end
+
describe '#auto_canceled?' do
subject { pipeline.auto_canceled? }
@@ -534,6 +546,22 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#has_kubernetes_active?' do
+ context 'when kubernetes is active' do
+ let(:project) { create(:kubernetes_project) }
+
+ it 'returns true' do
+ expect(pipeline).to have_kubernetes_active
+ end
+ end
+
+ context 'when kubernetes is not active' do
+ it 'returns false' do
+ expect(pipeline).not_to have_kubernetes_active
+ end
+ end
+ end
+
describe '#has_stage_seeds?' do
context 'when pipeline has stage seeds' do
subject { build(:ci_pipeline_with_one_job) }
@@ -771,14 +799,118 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#set_config_source' do
+ context 'on object initialisation' do
+ context 'when pipelines does not contain needed data' do
+ let(:pipeline) do
+ Ci::Pipeline.new
+ end
+
+ it 'defines source to be unknown' do
+ expect(pipeline).to be_unknown_source
+ end
+ end
+
+ context 'when pipeline contains all needed data' do
+ let(:pipeline) do
+ Ci::Pipeline.new(
+ project: project,
+ sha: '1234',
+ ref: 'master',
+ source: :push)
+ end
+
+ context 'when the repository has a config file' do
+ before do
+ allow(project.repository).to receive(:gitlab_ci_yml_for)
+ .and_return('config')
+ end
+
+ it 'defines source to be from repository' do
+ expect(pipeline).to be_repository_source
+ end
+
+ context 'when loading an object' do
+ let(:new_pipeline) { Ci::Pipeline.find(pipeline.id) }
+
+ it 'does not redefine the source' do
+ # force to overwrite the source
+ pipeline.unknown_source!
+
+ expect(new_pipeline).to be_unknown_source
+ end
+ end
+ end
+
+ context 'when the repository does not have a config file' do
+ let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content }
+
+ context 'auto devops enabled' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ allow(project).to receive(:ci_config_path) { 'custom' }
+ end
+
+ it 'defines source to be auto devops' do
+ subject
+
+ expect(pipeline).to be_auto_devops_source
+ end
+ end
+ end
+ end
+ end
+ end
+
describe '#ci_yaml_file' do
- it 'reports error if the file is not found' do
- allow(pipeline.project).to receive(:ci_config_path) { 'custom' }
+ let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content }
- pipeline.ci_yaml_file
+ context 'the source is unknown' do
+ before do
+ pipeline.unknown_source!
+ end
- expect(pipeline.yaml_errors)
- .to eq('Failed to load CI/CD config file at custom')
+ it 'returns the configuration if found' do
+ allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for)
+ .and_return('config')
+
+ expect(pipeline.ci_yaml_file).to be_a(String)
+ expect(pipeline.ci_yaml_file).not_to eq(implied_yml)
+ expect(pipeline.yaml_errors).to be_nil
+ end
+
+ it 'sets yaml errors if not found' do
+ expect(pipeline.ci_yaml_file).to be_nil
+ expect(pipeline.yaml_errors)
+ .to start_with('Failed to load CI/CD config file')
+ end
+ end
+
+ context 'the source is the repository' do
+ before do
+ pipeline.repository_source!
+ end
+
+ it 'returns the configuration if found' do
+ allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for)
+ .and_return('config')
+
+ expect(pipeline.ci_yaml_file).to be_a(String)
+ expect(pipeline.ci_yaml_file).not_to eq(implied_yml)
+ expect(pipeline.yaml_errors).to be_nil
+ end
+ end
+
+ context 'when the source is auto_devops_source' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ pipeline.auto_devops_source!
+ end
+
+ it 'finds the implied config' do
+ expect(pipeline.ci_yaml_file).to eq(implied_yml)
+ expect(pipeline.yaml_errors).to be_nil
+ end
end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 48f878bbee6..2e686e515c5 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
describe Ci::Runner do
describe 'validation' do
+ it { is_expected.to validate_presence_of(:access_level) }
+
context 'when runner is not allowed to pick untagged jobs' do
context 'when runner does not have tags' do
it 'is not valid' do
@@ -19,6 +21,34 @@ describe Ci::Runner do
end
end
+ describe '#access_level' do
+ context 'when creating new runner and access_level is nil' do
+ let(:runner) do
+ build(:ci_runner, access_level: nil)
+ end
+
+ it "object is invalid" do
+ expect(runner).not_to be_valid
+ end
+ end
+
+ context 'when creating new runner and access_level is defined in enum' do
+ let(:runner) do
+ build(:ci_runner, access_level: :not_protected)
+ end
+
+ it "object is valid" do
+ expect(runner).to be_valid
+ end
+ end
+
+ context 'when creating new runner and access_level is not defined in enum' do
+ it "raises an error" do
+ expect { build(:ci_runner, access_level: :this_is_not_defined) }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
describe '#display_name' do
it 'returns the description if it has a value' do
runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
@@ -95,6 +125,8 @@ describe Ci::Runner do
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:runner) { create(:ci_runner) }
+ subject { runner.can_pick?(build) }
+
before do
build.project.runners << runner
end
@@ -222,6 +254,50 @@ describe Ci::Runner do
end
end
end
+
+ context 'when access_level of runner is not_protected' do
+ before do
+ runner.not_protected!
+ end
+
+ context 'when build is protected' do
+ before do
+ build.protected = true
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when build is unprotected' do
+ before do
+ build.protected = false
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when access_level of runner is ref_protected' do
+ before do
+ runner.ref_protected!
+ end
+
+ context 'when build is protected' do
+ before do
+ build.protected = true
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when build is unprotected' do
+ before do
+ build.protected = false
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
describe '#status' do
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
new file mode 100644
index 00000000000..586d073eb5e
--- /dev/null
+++ b/spec/models/ci/stage_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Ci::Stage, :models do
+ let(:stage) { create(:ci_stage_entity) }
+
+ describe 'associations' do
+ before do
+ create(:ci_build, stage_id: stage.id)
+ create(:commit_status, stage_id: stage.id)
+ end
+
+ describe '#statuses' do
+ it 'returns all commit statuses' do
+ expect(stage.statuses.count).to be 2
+ end
+ end
+
+ describe '#builds' do
+ it 'returns only builds' do
+ expect(stage.builds).to be_one
+ end
+ end
+ end
+
+ describe '#status' do
+ context 'when stage is pending' do
+ let(:stage) { create(:ci_stage_entity, status: 'pending') }
+
+ it 'has a correct status value' do
+ expect(stage.status).to eq 'pending'
+ end
+ end
+
+ context 'when stage is success' do
+ let(:stage) { create(:ci_stage_entity, status: 'success') }
+
+ it 'has a correct status value' do
+ expect(stage.status).to eq 'success'
+ end
+ end
+
+ context 'when stage status is not defined' do
+ before do
+ stage.update_column(:status, nil)
+ end
+
+ it 'sets the default value' do
+ expect(described_class.find(stage.id).status)
+ .to eq 'created'
+ end
+ end
+ end
+
+ describe 'update_status' do
+ context 'when stage objects needs to be updated' do
+ before do
+ create(:ci_build, :success, stage_id: stage.id)
+ create(:ci_build, :running, stage_id: stage.id)
+ end
+
+ it 'updates stage status correctly' do
+ expect { stage.update_status }
+ .to change { stage.reload.status }
+ .to 'running'
+ end
+ end
+
+ context 'when stage is skipped' do
+ it 'updates status to skipped' do
+ expect { stage.update_status }
+ .to change { stage.reload.status }
+ .to 'skipped'
+ end
+ end
+
+ context 'when stage object is locked' do
+ before do
+ create(:ci_build, :failed, stage_id: stage.id)
+ end
+
+ it 'retries a lock to update a stage status' do
+ stage.lock_version = 100
+
+ stage.update_status
+
+ expect(stage.reload).to be_failed
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/trigger_request_spec.rb b/spec/models/ci/trigger_request_spec.rb
new file mode 100644
index 00000000000..7dcf3528f73
--- /dev/null
+++ b/spec/models/ci/trigger_request_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Ci::TriggerRequest do
+ describe 'validation' do
+ it 'be invalid if saving a variable' do
+ trigger = build(:ci_trigger_request, variables: { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } )
+
+ expect(trigger).not_to be_valid
+ end
+
+ it 'be valid if not saving a variable' do
+ trigger = build(:ci_trigger_request)
+
+ expect(trigger).to be_valid
+ end
+ end
+end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 08693b5da33..11e64a0f877 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -33,7 +33,6 @@ describe Commit do
describe '#to_reference' do
let(:project) { create(:project, :repository, path: 'sample-project') }
- let(:commit) { project.commit }
it 'returns a String reference to the object' do
expect(commit.to_reference).to eq commit.id
@@ -47,7 +46,6 @@ describe Commit do
describe '#reference_link_text' do
let(:project) { create(:project, :repository, path: 'sample-project') }
- let(:commit) { project.commit }
it 'returns a String reference to the object' do
expect(commit.reference_link_text).to eq commit.short_id
@@ -191,12 +189,73 @@ eos
it { expect(data).to be_a(Hash) }
it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') }
- it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46+00:00') }
+ it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46Z') }
it { expect(data[:added]).to eq(["bar/branch-test.txt"]) }
it { expect(data[:modified]).to eq([]) }
it { expect(data[:removed]).to eq([]) }
end
+ describe '#cherry_pick_message' do
+ let(:user) { create(:user) }
+
+ context 'of a regular commit' do
+ let(:commit) { project.commit('video') }
+
+ it { expect(commit.cherry_pick_message(user)).to include("\n\n(cherry picked from commit 88790590ed1337ab189bccaa355f068481c90bec)") }
+ end
+
+ 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',
+ target_branch: 'master',
+ source_project: project,
+ author: user)
+ end
+
+ let(:merge_commit) do
+ merge_commit_id = repository.merge(user,
+ merge_request.diff_head_sha,
+ merge_request,
+ commit_options)
+
+ repository.commit(merge_commit_id)
+ end
+
+ context 'that is found' do
+ before do
+ # Artificially mark as completed.
+ merge_request.update(merge_commit_sha: merge_commit.id)
+ end
+
+ it do
+ expected_appended_text = <<~STR.rstrip
+
+ (cherry picked from commit #{merge_commit.sha})
+
+ 467dc98f Add new 'videos' directory
+ 88790590 Upload new video file
+ STR
+
+ expect(merge_commit.cherry_pick_message(user)).to include(expected_appended_text)
+ end
+ end
+
+ context "that is existing but not found" do
+ it 'does not include details of the merged commits' do
+ expect(merge_commit.cherry_pick_message(user)).to end_with("(cherry picked from commit #{merge_commit.sha})")
+ end
+ end
+ end
+ end
+
describe '#reverts_commit?' do
let(:another_commit) { double(:commit, revert_description: "This reverts commit #{commit.sha}") }
let(:user) { commit.author }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 6fb4794ea5f..858ec831200 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -7,10 +7,10 @@ describe CommitStatus do
create(:ci_pipeline, project: project, sha: project.commit.id)
end
- let(:commit_status) { create_status }
+ let(:commit_status) { create_status(stage: 'test') }
- def create_status(args = {})
- create(:commit_status, args.merge(pipeline: pipeline))
+ def create_status(**opts)
+ create(:commit_status, pipeline: pipeline, **opts)
end
it { is_expected.to belong_to(:pipeline) }
@@ -425,7 +425,7 @@ describe CommitStatus do
end
it "raise exception when trying to update" do
- expect{ commit_status.save }.to raise_error(ActiveRecord::StaleObjectError)
+ expect { commit_status.save }.to raise_error(ActiveRecord::StaleObjectError)
end
end
@@ -443,4 +443,25 @@ describe CommitStatus do
end
end
end
+
+ describe 'set failure_reason when drop' do
+ let(:commit_status) { create(:commit_status, :created) }
+
+ subject do
+ commit_status.drop!(reason)
+ commit_status
+ end
+
+ context 'when failure_reason is nil' do
+ let(:reason) { }
+
+ it { is_expected.to be_unknown_failure }
+ end
+
+ context 'when failure_reason is script_failure' do
+ let(:reason) { :script_failure }
+
+ it { is_expected.to be_script_failure }
+ end
+ end
end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index 63ad3a3630b..34f923d3f0c 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -12,17 +12,25 @@ describe Awardable do
describe "ClassMethods" do
let!(:issue2) { create(:issue) }
+ let!(:award_emoji2) { create(:award_emoji, awardable: issue2) }
- before do
- create(:award_emoji, awardable: issue2)
- end
+ describe "orders" do
+ it "orders on upvotes" do
+ expect(Issue.order_upvotes_desc.to_a).to eq [issue2, issue]
+ end
- it "orders on upvotes" do
- expect(Issue.order_upvotes_desc.to_a).to eq [issue2, issue]
+ it "orders on downvotes" do
+ expect(Issue.order_downvotes_desc.to_a).to eq [issue, issue2]
+ end
end
- it "orders on downvotes" do
- expect(Issue.order_downvotes_desc.to_a).to eq [issue, issue2]
+ describe ".awarded" do
+ it "filters by user and emoji name" do
+ expect(Issue.awarded(award_emoji.user, "thumbsup")).to be_empty
+ expect(Issue.awarded(award_emoji.user, "thumbsdown")).to eq [issue]
+ expect(Issue.awarded(award_emoji2.user, "thumbsup")).to eq [issue2]
+ expect(Issue.awarded(award_emoji2.user, "thumbsdown")).to be_empty
+ end
end
end
diff --git a/spec/models/concerns/editable_spec.rb b/spec/models/concerns/editable_spec.rb
index cd73af3b480..49a9a8ebcbc 100644
--- a/spec/models/concerns/editable_spec.rb
+++ b/spec/models/concerns/editable_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
describe Editable do
- describe '#is_edited?' do
+ describe '#edited?' do
let(:issue) { create(:issue, last_edited_at: nil) }
let(:edited_issue) { create(:issue, created_at: 3.days.ago, last_edited_at: 2.days.ago) }
- it { expect(issue.is_edited?).to eq(false) }
- it { expect(edited_issue.is_edited?).to eq(true) }
+ it { expect(issue.edited?).to eq(false) }
+ it { expect(edited_issue.edited?).to eq(true) }
end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 0137f71be8f..fb5fb7daaab 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -66,56 +66,76 @@ describe Issuable do
end
describe ".search" do
- let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
+ let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
- it 'returns notes with a matching title' do
+ it 'returns issues with a matching title' do
expect(issuable_class.search(searchable_issue.title))
.to eq([searchable_issue])
end
- it 'returns notes with a partially matching title' do
+ it 'returns issues with a partially matching title' do
expect(issuable_class.search('able')).to eq([searchable_issue])
end
- it 'returns notes with a matching title regardless of the casing' do
+ it 'returns issues with a matching title regardless of the casing' do
expect(issuable_class.search(searchable_issue.title.upcase))
.to eq([searchable_issue])
end
+
+ it 'returns issues with a fuzzy matching title' do
+ expect(issuable_class.search('searchable issue')).to eq([searchable_issue])
+ end
+
+ it 'returns all issues with a query shorter than 3 chars' do
+ expect(issuable_class.search('zz')).to eq(issuable_class.all)
+ end
end
describe ".full_search" do
let!(:searchable_issue) do
- create(:issue, title: "Searchable issue", description: 'kittens')
+ create(:issue, title: "Searchable awesome issue", description: 'Many cute kittens')
end
- it 'returns notes with a matching title' do
+ it 'returns issues with a matching title' do
expect(issuable_class.full_search(searchable_issue.title))
.to eq([searchable_issue])
end
- it 'returns notes with a partially matching title' do
+ it 'returns issues with a partially matching title' do
expect(issuable_class.full_search('able')).to eq([searchable_issue])
end
- it 'returns notes with a matching title regardless of the casing' do
+ it 'returns issues with a matching title regardless of the casing' do
expect(issuable_class.full_search(searchable_issue.title.upcase))
.to eq([searchable_issue])
end
- it 'returns notes with a matching description' do
+ it 'returns issues with a fuzzy matching title' do
+ expect(issuable_class.full_search('searchable issue')).to eq([searchable_issue])
+ end
+
+ it 'returns issues with a matching description' do
expect(issuable_class.full_search(searchable_issue.description))
.to eq([searchable_issue])
end
- it 'returns notes with a partially matching description' do
+ it 'returns issues with a partially matching description' do
expect(issuable_class.full_search(searchable_issue.description))
.to eq([searchable_issue])
end
- it 'returns notes with a matching description regardless of the casing' do
+ it 'returns issues with a matching description regardless of the casing' do
expect(issuable_class.full_search(searchable_issue.description.upcase))
.to eq([searchable_issue])
end
+
+ it 'returns issues with a fuzzy matching description' do
+ expect(issuable_class.full_search('many kittens')).to eq([searchable_issue])
+ end
+
+ it 'returns all issues with a query shorter than 3 chars' do
+ expect(issuable_class.search('zz')).to eq(issuable_class.all)
+ end
end
describe '.to_ability_name' do
@@ -300,7 +320,7 @@ describe Issuable do
let(:bug) { create(:label, project: project, title: 'bug') }
let(:issue) { create(:issue, project: project) }
- before(:each) do
+ before do
issue.labels << bug
end
@@ -402,7 +422,7 @@ describe Issuable do
let(:issue2) { create(:issue, title: "Bugfix2", project: project) }
let(:issue3) { create(:issue, title: "Feature1", project: project) }
- before(:each) do
+ before do
issue1.labels << bug
issue1.labels << feature
issue2.labels << bug
@@ -460,4 +480,71 @@ describe Issuable do
end
end
end
+
+ describe '#first_contribution?' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+ let(:other_project) { create(:project) }
+ let(:owner) { create(:owner) }
+ let(:master) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
+
+ let(:contributor) { create(:user) }
+ let(:first_time_contributor) { create(:user) }
+
+ before do
+ group.add_owner(owner)
+ project.add_master(master)
+ project.add_reporter(reporter)
+ project.add_guest(guest)
+ project.add_guest(contributor)
+ project.add_guest(first_time_contributor)
+ end
+
+ let(:merged_mr) { create(:merge_request, :merged, author: contributor, target_project: project, source_project: project) }
+ let(:open_mr) { create(:merge_request, author: first_time_contributor, target_project: project, source_project: project) }
+ let(:merged_mr_other_project) { create(:merge_request, :merged, author: first_time_contributor, target_project: other_project, source_project: other_project) }
+
+ context "for merge requests" do
+ it "is false for MASTER" do
+ mr = create(:merge_request, author: master, target_project: project, source_project: project)
+
+ expect(mr).not_to be_first_contribution
+ end
+
+ it "is false for OWNER" do
+ mr = create(:merge_request, author: owner, target_project: project, source_project: project)
+
+ expect(mr).not_to be_first_contribution
+ end
+
+ it "is false for REPORTER" do
+ mr = create(:merge_request, author: reporter, target_project: project, source_project: project)
+
+ expect(mr).not_to be_first_contribution
+ end
+
+ it "is true when you don't have any merged MR" do
+ expect(open_mr).to be_first_contribution
+ expect(merged_mr).not_to be_first_contribution
+ end
+
+ it "handles multiple projects separately" do
+ expect(open_mr).to be_first_contribution
+ expect(merged_mr_other_project).not_to be_first_contribution
+ end
+ end
+
+ context "for issues" do
+ let(:contributor_issue) { create(:issue, author: contributor, project: project) }
+ let(:first_time_contributor_issue) { create(:issue, author: first_time_contributor, project: project) }
+
+ it "is false even without merged MR" do
+ expect(merged_mr).to be
+ expect(first_time_contributor_issue).not_to be_first_contribution
+ expect(contributor_issue).not_to be_first_contribution
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index 5f9b7e0a367..a5d505af001 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -31,7 +31,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
let(:now) { Time.now.utc }
- around(:each) do |example|
+ around do |example|
Timecop.freeze(now) { example.run }
end
diff --git a/spec/models/concerns/resolvable_note_spec.rb b/spec/models/concerns/resolvable_note_spec.rb
index d00faa4f8be..91591017587 100644
--- a/spec/models/concerns/resolvable_note_spec.rb
+++ b/spec/models/concerns/resolvable_note_spec.rb
@@ -189,8 +189,8 @@ describe Note, ResolvableNote do
allow(subject).to receive(:resolvable?).and_return(false)
end
- it "returns nil" do
- expect(subject.resolve!(current_user)).to be_nil
+ it "returns false" do
+ expect(subject.resolve!(current_user)).to be_falsey
end
it "doesn't set resolved_at" do
@@ -224,8 +224,8 @@ describe Note, ResolvableNote do
subject.resolve!(user)
end
- it "returns nil" do
- expect(subject.resolve!(current_user)).to be_nil
+ it "returns false" do
+ expect(subject.resolve!(current_user)).to be_falsey
end
it "doesn't change resolved_at" do
@@ -279,8 +279,8 @@ describe Note, ResolvableNote do
allow(subject).to receive(:resolvable?).and_return(false)
end
- it "returns nil" do
- expect(subject.unresolve!).to be_nil
+ it "returns false" do
+ expect(subject.unresolve!).to be_falsey
end
end
@@ -320,8 +320,8 @@ describe Note, ResolvableNote do
end
context "when not resolved" do
- it "returns nil" do
- expect(subject.unresolve!).to be_nil
+ it "returns false" do
+ expect(subject.unresolve!).to be_falsey
end
end
end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index bae88cb1d24..e46945e301e 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe ContainerRepository do
let(:group) { create(:group, name: 'group') }
- let(:project) { create(:project, :repository, path: 'test', group: group) }
+ let(:project) { create(:project, path: 'test', group: group) }
let(:repository) do
create(:container_repository, name: 'my_image', project: project)
diff --git a/spec/models/event_collection_spec.rb b/spec/models/event_collection_spec.rb
new file mode 100644
index 00000000000..e0a87c18cc7
--- /dev/null
+++ b/spec/models/event_collection_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe EventCollection do
+ describe '#to_a' do
+ let(:project) { create(:project_empty_repo) }
+ let(:projects) { Project.where(id: project.id) }
+ let(:user) { create(:user) }
+
+ before do
+ 20.times do
+ event = create(:push_event, project: project, author: user)
+
+ create(:push_event_payload, event: event)
+ end
+
+ create(:closed_issue_event, project: project, author: user)
+ end
+
+ it 'returns an Array of events' do
+ events = described_class.new(projects).to_a
+
+ expect(events).to be_an_instance_of(Array)
+ end
+
+ it 'applies a limit to the number of events' do
+ events = described_class.new(projects).to_a
+
+ expect(events.length).to eq(20)
+ end
+
+ it 'can paginate through events' do
+ events = described_class.new(projects, offset: 20).to_a
+
+ expect(events.length).to eq(1)
+ end
+
+ it 'returns an empty Array when crossing the maximum page number' do
+ events = described_class.new(projects, limit: 1, offset: 15).to_a
+
+ expect(events).to be_empty
+ end
+
+ it 'allows filtering of events using an EventFilter' do
+ filter = EventFilter.new(EventFilter.issue)
+ events = described_class.new(projects, filter: filter).to_a
+
+ expect(events.length).to eq(1)
+ expect(events[0].action).to eq(Event::CLOSED)
+ end
+ end
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index d86bf1a90a9..aa7a8342a4c 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -11,7 +11,6 @@ describe Event do
it { is_expected.to respond_to(:author_email) }
it { is_expected.to respond_to(:issue_title) }
it { is_expected.to respond_to(:merge_request_title) }
- it { is_expected.to respond_to(:commits) }
end
describe 'Callbacks' do
@@ -304,27 +303,59 @@ describe Event do
end
end
- def create_push_event(project, user, attrs = {})
- data = {
- before: Gitlab::Git::BLANK_SHA,
- after: "0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e",
- ref: "refs/heads/master",
- user_id: user.id,
- user_name: user.name,
- repository: {
- name: project.name,
- url: "localhost/rubinius",
- description: "",
- homepage: "localhost/rubinius",
- private: true
- }
- }
-
- described_class.create({
- project: project,
- action: described_class::PUSHED,
- data: data,
- author_id: user.id
- }.merge!(attrs))
+ describe '#body?' do
+ let(:push_event) do
+ event = build(:push_event)
+
+ allow(event).to receive(:push?).and_return(true)
+
+ event
+ end
+
+ it 'returns true for a push event with commits' do
+ allow(push_event).to receive(:push_with_commits?).and_return(true)
+
+ expect(push_event).to be_body
+ end
+
+ it 'returns false for a push event without a valid commit range' do
+ allow(push_event).to receive(:push_with_commits?).and_return(false)
+
+ expect(push_event).not_to be_body
+ end
+
+ it 'returns true for a Note event' do
+ event = build(:event)
+
+ allow(event).to receive(:note?).and_return(true)
+
+ expect(event).to be_body
+ end
+
+ it 'returns true if the target responds to #title' do
+ event = build(:event)
+
+ allow(event).to receive(:target).and_return(double(:target, title: 'foo'))
+
+ expect(event).to be_body
+ end
+
+ it 'returns false for a regular event without a target' do
+ event = build(:event)
+
+ expect(event).not_to be_body
+ end
+ end
+
+ def create_push_event(project, user)
+ event = create(:push_event, project: project, author: user)
+
+ create(:push_event_payload,
+ event: event,
+ commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ commit_count: 0,
+ ref: 'master')
+
+ event
end
end
diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb
index e48f20bf53b..9c99c3e5c08 100644
--- a/spec/models/gpg_key_spec.rb
+++ b/spec/models/gpg_key_spec.rb
@@ -99,14 +99,14 @@ describe GpgKey do
end
describe '#verified?' do
- it 'returns true one of the email addresses in the key belongs to the user' do
+ it 'returns true if one of the email addresses in the key belongs to the user' do
user = create :user, email: 'bette.cartwright@example.com'
gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
expect(gpg_key.verified?).to be_truthy
end
- it 'returns false if one of the email addresses in the key does not belong to the user' do
+ it 'returns false if none of the email addresses in the key does not belong to the user' do
user = create :user, email: 'someone.else@example.com'
gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
@@ -114,6 +114,32 @@ describe GpgKey do
end
end
+ describe 'verified_and_belongs_to_email?' do
+ it 'returns false if none of the email addresses in the key does not belong to the user' do
+ user = create :user, email: 'someone.else@example.com'
+ gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
+
+ expect(gpg_key.verified?).to be_falsey
+ expect(gpg_key.verified_and_belongs_to_email?('someone.else@example.com')).to be_falsey
+ end
+
+ it 'returns false if one of the email addresses in the key belongs to the user and does not match the provided email' do
+ user = create :user, email: 'bette.cartwright@example.com'
+ gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key, user: user
+
+ expect(gpg_key.verified?).to be_truthy
+ expect(gpg_key.verified_and_belongs_to_email?('bette.cartwright@example.net')).to be_falsey
+ end
+
+ it 'returns true if one of the email addresses in the key belongs to the user and 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
+
+ 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) }
@@ -129,15 +155,15 @@ describe GpgKey do
describe '#revoke' do
it 'invalidates all associated gpg signatures and destroys the key' do
gpg_key = create :gpg_key
- gpg_signature = create :gpg_signature, valid_signature: true, gpg_key: gpg_key
+ gpg_signature = create :gpg_signature, verification_status: :verified, gpg_key: gpg_key
unrelated_gpg_key = create :gpg_key, key: GpgHelpers::User2.public_key
- unrelated_gpg_signature = create :gpg_signature, valid_signature: true, gpg_key: unrelated_gpg_key
+ unrelated_gpg_signature = create :gpg_signature, verification_status: :verified, gpg_key: unrelated_gpg_key
gpg_key.revoke
expect(gpg_signature.reload).to have_attributes(
- valid_signature: false,
+ verification_status: 'unknown_key',
gpg_key: nil
)
@@ -145,7 +171,7 @@ describe GpgKey do
# unrelated signature is left untouched
expect(unrelated_gpg_signature.reload).to have_attributes(
- valid_signature: true,
+ verification_status: 'verified',
gpg_key: unrelated_gpg_key
)
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index c5bfae47606..f36d6eeb327 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -9,6 +9,7 @@ describe Group do
it { is_expected.to have_many(:users).through(:group_members) }
it { is_expected.to have_many(:owners).through(:group_members) }
it { is_expected.to have_many(:requesters).dependent(:destroy) }
+ it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
@@ -25,22 +26,8 @@ describe Group do
group.add_developer(developer)
end
- describe '#members' do
- it 'includes members and exclude requesters' do
- member_user_ids = group.members.pluck(:user_id)
-
- expect(member_user_ids).to include(developer.id)
- expect(member_user_ids).not_to include(requester.id)
- end
- end
-
- describe '#requesters' do
- it 'does not include requesters' do
- requester_user_ids = group.requesters.pluck(:user_id)
-
- expect(requester_user_ids).to include(requester.id)
- expect(requester_user_ids).not_to include(developer.id)
- end
+ it_behaves_like 'members and requesters associations' do
+ let(:namespace) { group }
end
end
end
@@ -84,6 +71,83 @@ describe Group do
expect(group).not_to be_valid
end
end
+
+ describe '#visibility_level_allowed_by_parent' do
+ let(:parent) { create(:group, :internal) }
+ let(:sub_group) { build(:group, parent_id: parent.id) }
+
+ context 'without a parent' do
+ it 'is valid' do
+ sub_group.parent_id = nil
+
+ expect(sub_group).to be_valid
+ end
+ end
+
+ context 'with a parent' do
+ context 'when visibility of sub group is greater than the parent' do
+ it 'is invalid' do
+ sub_group.visibility_level = Gitlab::VisibilityLevel::PUBLIC
+
+ expect(sub_group).to be_invalid
+ end
+ end
+
+ context 'when visibility of sub group is lower or equal to the parent' do
+ [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE].each do |level|
+ it 'is valid' do
+ sub_group.visibility_level = level
+
+ expect(sub_group).to be_valid
+ end
+ end
+ end
+ end
+ end
+
+ describe '#visibility_level_allowed_by_projects' do
+ let!(:internal_group) { create(:group, :internal) }
+ let!(:internal_project) { create(:project, :internal, group: internal_group) }
+
+ context 'when group has a lower visibility' do
+ it 'is invalid' do
+ internal_group.visibility_level = Gitlab::VisibilityLevel::PRIVATE
+
+ expect(internal_group).to be_invalid
+ expect(internal_group.errors[:visibility_level]).to include('private is not allowed since this group contains projects with higher visibility.')
+ end
+ end
+
+ context 'when group has a higher visibility' do
+ it 'is valid' do
+ internal_group.visibility_level = Gitlab::VisibilityLevel::PUBLIC
+
+ expect(internal_group).to be_valid
+ end
+ end
+ end
+
+ describe '#visibility_level_allowed_by_sub_groups' do
+ let!(:internal_group) { create(:group, :internal) }
+ let!(:internal_sub_group) { create(:group, :internal, parent: internal_group) }
+
+ context 'when parent group has a lower visibility' do
+ it 'is invalid' do
+ internal_group.visibility_level = Gitlab::VisibilityLevel::PRIVATE
+
+ expect(internal_group).to be_invalid
+ expect(internal_group.errors[:visibility_level]).to include('private is not allowed since there are sub-groups with higher visibility.')
+ end
+ end
+
+ context 'when parent group has a higher visibility' do
+ it 'is valid' do
+ internal_group.visibility_level = Gitlab::VisibilityLevel::PUBLIC
+
+ expect(internal_group).to be_valid
+ end
+ end
+ end
end
describe '.visible_to_user' do
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index c055863d298..e547da0cfbe 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -57,18 +57,14 @@ describe Issue do
end
describe '#closed_at' do
- after do
- Timecop.return
- end
-
- let!(:now) { Timecop.freeze(Time.now) }
-
it 'sets closed_at to Time.now when issue is closed' do
issue = create(:issue, state: 'opened')
+ expect(issue.closed_at).to be_nil
+
issue.close
- expect(issue.closed_at).to eq(now)
+ expect(issue.closed_at).to be_present
end
end
@@ -350,7 +346,7 @@ describe Issue do
subject { create(:issue, project: create(:project, :repository)) }
let(:backref_text) { "issue #{subject.to_reference}" }
- let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
+ let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
end
it_behaves_like 'a Taskable' do
@@ -755,4 +751,40 @@ describe Issue do
end
end
end
+
+ describe 'removing an issue' do
+ it 'refreshes the number of open issues of the project' do
+ project = subject.project
+
+ expect { subject.destroy }
+ .to change { project.open_issues_count }.from(1).to(0)
+ end
+ end
+
+ describe '.public_only' do
+ it 'only returns public issues' do
+ public_issue = create(:issue)
+ create(:issue, confidential: true)
+
+ expect(described_class.public_only).to eq([public_issue])
+ end
+ end
+
+ describe '#update_project_counter_caches?' do
+ it 'returns true when the state changes' do
+ subject.state = 'closed'
+
+ expect(subject.update_project_counter_caches?).to eq(true)
+ end
+
+ it 'returns true when the confidential flag changes' do
+ subject.confidential = true
+
+ expect(subject.update_project_counter_caches?).to eq(true)
+ end
+
+ it 'returns false when the state or confidential flag did not change' do
+ expect(subject.update_project_counter_caches?).to eq(false)
+ end
+ end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 3508391c721..96baeaff0a4 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -1,6 +1,13 @@
require 'spec_helper'
describe Key, :mailer do
+ include Gitlab::CurrentSettings
+
+ describe 'modules' do
+ subject { described_class }
+ it { is_expected.to include_module(Gitlab::CurrentSettings) }
+ end
+
describe "Associations" do
it { is_expected.to belong_to(:user) }
end
@@ -11,8 +18,10 @@ describe Key, :mailer do
it { is_expected.to validate_presence_of(:key) }
it { is_expected.to validate_length_of(:key).is_at_most(5000) }
- it { is_expected.to allow_value('ssh-foo').for(:key) }
- it { is_expected.to allow_value('ecdsa-foo').for(:key) }
+ it { is_expected.to allow_value(attributes_for(:rsa_key_2048)[:key]).for(:key) }
+ it { is_expected.to allow_value(attributes_for(:dsa_key_2048)[:key]).for(:key) }
+ it { is_expected.to allow_value(attributes_for(:ecdsa_key_256)[:key]).for(:key) }
+ it { is_expected.to allow_value(attributes_for(:ed25519_key_256)[:key]).for(:key) }
it { is_expected.not_to allow_value('foo-bar').for(:key) }
end
@@ -95,6 +104,48 @@ describe Key, :mailer do
end
end
+ context 'validate it meets key restrictions' do
+ where(:factory, :minimum, :result) do
+ forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE
+
+ [
+ [:rsa_key_2048, 0, true],
+ [:dsa_key_2048, 0, true],
+ [:ecdsa_key_256, 0, true],
+ [:ed25519_key_256, 0, true],
+
+ [:rsa_key_2048, 1024, true],
+ [:rsa_key_2048, 2048, true],
+ [:rsa_key_2048, 4096, false],
+
+ [:dsa_key_2048, 1024, true],
+ [:dsa_key_2048, 2048, true],
+ [:dsa_key_2048, 4096, false],
+
+ [:ecdsa_key_256, 256, true],
+ [:ecdsa_key_256, 384, false],
+
+ [:ed25519_key_256, 256, true],
+ [:ed25519_key_256, 384, false],
+
+ [:rsa_key_2048, forbidden, false],
+ [:dsa_key_2048, forbidden, false],
+ [:ecdsa_key_256, forbidden, false],
+ [:ed25519_key_256, forbidden, false]
+ ]
+ end
+
+ with_them do
+ subject(:key) { build(factory) }
+
+ before do
+ stub_application_setting("#{key.public_key.type}_key_restriction" => minimum)
+ end
+
+ it { expect(key.valid?).to eq(result) }
+ end
+ end
+
context 'callbacks' do
it 'adds new key to authorized_file' do
key = build(:personal_key, id: 7)
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 87513e18b25..a07ce05a865 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -409,6 +409,15 @@ describe Member do
expect(members).to be_a Array
expect(members).to be_empty
end
+
+ it 'supports differents formats' do
+ list = ['joe@local.test', admin, user1.id, user2.id.to_s]
+
+ members = described_class.add_users(source, list, :master)
+
+ expect(members.size).to eq(4)
+ expect(members.first).to be_invite
+ end
end
end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index f1d1f37c78a..fa3e80ba062 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -149,7 +149,7 @@ describe ProjectMember do
describe 'notifications' do
describe '#after_accept_request' do
it 'calls NotificationService.new_project_member' do
- member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now)
+ member = create(:project_member, user: create(:user), requested_at: Time.now)
expect_any_instance_of(NotificationService).to receive(:new_project_member)
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index a1a3e70a7d2..d80d5657c42 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -159,6 +159,7 @@ describe MergeRequest do
before do
subject.project.has_external_issue_tracker = true
subject.project.save!
+ create(:jira_service, project: subject.project)
end
it 'does not cache issues from external trackers' do
@@ -166,6 +167,7 @@ describe MergeRequest do
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
+ expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to raise_error
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
end
@@ -604,7 +606,7 @@ describe MergeRequest do
request = build_stubbed(:merge_request)
expect(request.merge_commit_message)
- .to match("See merge request #{request.to_reference}")
+ .to match("See merge request #{request.to_reference(full: true)}")
end
it 'excludes multiple linebreak runs when description is blank' do
@@ -681,7 +683,7 @@ describe MergeRequest do
end
it 'does not crash' do
- expect{ subject.diverged_commits_count }.not_to raise_error
+ expect { subject.diverged_commits_count }.not_to raise_error
end
it 'returns 0' do
@@ -714,7 +716,7 @@ describe MergeRequest do
end
describe 'caching' do
- before(:example) do
+ before do
allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
end
@@ -753,7 +755,7 @@ describe MergeRequest do
subject { create(:merge_request, :simple) }
let(:backref_text) { "merge request #{subject.to_reference}" }
- let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
+ let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
end
it_behaves_like 'a Taskable' do
@@ -931,6 +933,23 @@ describe MergeRequest do
end
end
+ describe '#merge_async' do
+ it 'enqueues MergeWorker job and updates merge_jid' do
+ merge_request = create(:merge_request)
+ user_id = double(:user_id)
+ params = double(:params)
+ merge_jid = 'hash-123'
+
+ expect(MergeWorker).to receive(:perform_async).with(merge_request.id, user_id, params) do
+ merge_jid
+ end
+
+ merge_request.merge_async(user_id, params)
+
+ expect(merge_request.reload.merge_jid).to eq(merge_jid)
+ end
+ end
+
describe '#check_if_can_be_merged' do
let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) }
@@ -1243,7 +1262,6 @@ describe MergeRequest do
describe "#reload_diff" do
let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
-
let(:commit) { subject.project.commit(sample_commit.id) }
it "does not change existing merge request diff" do
@@ -1261,9 +1279,19 @@ describe MergeRequest do
subject.reload_diff
end
- it "updates diff discussion positions" do
- old_diff_refs = subject.diff_refs
+ it "calls update_diff_discussion_positions" do
+ expect(subject).to receive(:update_diff_discussion_positions)
+
+ subject.reload_diff
+ end
+ end
+
+ describe '#update_diff_discussion_positions' do
+ let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
+ let(:commit) { subject.project.commit(sample_commit.id) }
+ let(:old_diff_refs) { subject.diff_refs }
+ before do
# Update merge_request_diff so that #diff_refs will return commit.diff_refs
allow(subject).to receive(:create_merge_request_diff) do
subject.merge_request_diffs.create(
@@ -1274,7 +1302,9 @@ describe MergeRequest do
subject.merge_request_diff(true)
end
+ end
+ it "updates diff discussion positions" do
expect(Discussions::UpdateDiffPositionService).to receive(:new).with(
subject.project,
subject.author,
@@ -1286,7 +1316,26 @@ describe MergeRequest do
expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original
expect_any_instance_of(DiffNote).to receive(:save).once
- subject.reload_diff(subject.author)
+ subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
+ new_diff_refs: commit.diff_refs,
+ current_user: subject.author)
+ end
+
+ context 'when resolve_outdated_diff_discussions is set' do
+ before do
+ discussion
+
+ subject.project.update!(resolve_outdated_diff_discussions: true)
+ end
+
+ it 'calls MergeRequests::ResolvedDiscussionNotificationService' do
+ expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService)
+ .to receive(:execute).with(subject)
+
+ subject.update_diff_discussion_positions(old_diff_refs: old_diff_refs,
+ new_diff_refs: commit.diff_refs,
+ current_user: subject.author)
+ end
end
end
@@ -1370,29 +1419,11 @@ describe MergeRequest do
end
describe '#merge_ongoing?' do
- it 'returns true when merge process is ongoing for merge_jid' do
- merge_request = create(:merge_request, merge_jid: 'foo')
-
- allow(Gitlab::SidekiqStatus).to receive(:num_running).with(['foo']).and_return(1)
+ it 'returns true when merge_id is present and MR is not merged' do
+ merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
expect(merge_request.merge_ongoing?).to be(true)
end
-
- it 'returns false when no merge process running for merge_jid' do
- merge_request = build(:merge_request, merge_jid: 'foo')
-
- allow(Gitlab::SidekiqStatus).to receive(:num_running).with(['foo']).and_return(0)
-
- expect(merge_request.merge_ongoing?).to be(false)
- end
-
- it 'returns false when merge_jid is nil' do
- merge_request = build(:merge_request, merge_jid: nil)
-
- expect(Gitlab::SidekiqStatus).not_to receive(:num_running)
-
- expect(merge_request.merge_ongoing?).to be(false)
- end
end
describe "#closed_without_fork?" do
@@ -1692,4 +1723,25 @@ describe MergeRequest do
expect(subject.ref_fetched?).to be_falsey
end
end
+
+ describe 'removing a merge request' do
+ it 'refreshes the number of open merge requests of the target project' do
+ project = subject.target_project
+
+ expect { subject.destroy }
+ .to change { project.open_merge_requests_count }.from(1).to(0)
+ end
+ end
+
+ describe '#update_project_counter_caches?' do
+ it 'returns true when the state changes' do
+ subject.state = 'closed'
+
+ expect(subject.update_project_counter_caches?).to eq(true)
+ end
+
+ it 'returns false when the state did not change' do
+ expect(subject.update_project_counter_caches?).to eq(false)
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 1a00c50690c..81d5ab7a6d3 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -315,6 +315,20 @@ describe Namespace do
end
end
+ describe '#self_and_ancestors', :nested_groups do
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+ let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+
+ it 'returns the correct ancestors' do
+ expect(very_deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ expect(deep_nested_group.self_and_ancestors).to contain_exactly(group, nested_group, deep_nested_group)
+ expect(nested_group.self_and_ancestors).to contain_exactly(group, nested_group)
+ expect(group.self_and_ancestors).to contain_exactly(group)
+ end
+ end
+
describe '#descendants', :nested_groups do
let!(:group) { create(:group, path: 'git_lab') }
let!(:nested_group) { create(:group, parent: group) }
@@ -331,6 +345,22 @@ describe Namespace do
end
end
+ describe '#self_and_descendants', :nested_groups do
+ let!(:group) { create(:group, path: 'git_lab') }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:deep_nested_group) { create(:group, parent: nested_group) }
+ let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+ let!(:another_group) { create(:group, path: 'gitllab') }
+ let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
+
+ it 'returns the correct descendants' do
+ expect(very_deep_nested_group.self_and_descendants).to contain_exactly(very_deep_nested_group)
+ expect(deep_nested_group.self_and_descendants).to contain_exactly(deep_nested_group, very_deep_nested_group)
+ expect(nested_group.self_and_descendants).to contain_exactly(nested_group, deep_nested_group, very_deep_nested_group)
+ expect(group.self_and_descendants).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
+ end
+ end
+
describe '#users_with_descendants', :nested_groups do
let(:user_a) { create(:user) }
let(:user_b) { create(:user) }
@@ -374,6 +404,118 @@ describe Namespace do
let!(:project1) { create(:project_empty_repo, namespace: group) }
let!(:project2) { create(:project_empty_repo, namespace: child) }
- it { expect(group.all_projects.to_a).to eq([project2, project1]) }
+ it { expect(group.all_projects.to_a).to match_array([project2, project1]) }
+ end
+
+ describe '#share_with_group_lock with subgroups', :nested_groups do
+ context 'when creating a subgroup' do
+ let(:subgroup) { create(:group, parent: root_group )}
+
+ context 'under a parent with "Share with group lock" enabled' do
+ let(:root_group) { create(:group, share_with_group_lock: true) }
+
+ it 'enables "Share with group lock" on the subgroup' do
+ expect(subgroup.share_with_group_lock).to be_truthy
+ end
+ end
+
+ context 'under a parent with "Share with group lock" disabled' do
+ let(:root_group) { create(:group) }
+
+ it 'does not enable "Share with group lock" on the subgroup' do
+ expect(subgroup.share_with_group_lock).to be_falsey
+ end
+ end
+ end
+
+ context 'when enabling the parent group "Share with group lock"' do
+ let(:root_group) { create(:group) }
+ let!(:subgroup) { create(:group, parent: root_group )}
+
+ it 'the subgroup "Share with group lock" becomes enabled' do
+ root_group.update!(share_with_group_lock: true)
+
+ expect(subgroup.reload.share_with_group_lock).to be_truthy
+ end
+ end
+
+ context 'when disabling the parent group "Share with group lock" (which was already enabled)' do
+ let(:root_group) { create(:group, share_with_group_lock: true) }
+
+ context 'and the subgroup "Share with group lock" is enabled' do
+ let(:subgroup) { create(:group, parent: root_group, share_with_group_lock: true )}
+
+ it 'the subgroup "Share with group lock" does not change' do
+ root_group.update!(share_with_group_lock: false)
+
+ expect(subgroup.reload.share_with_group_lock).to be_truthy
+ end
+ end
+
+ context 'but the subgroup "Share with group lock" is disabled' do
+ let(:subgroup) { create(:group, parent: root_group )}
+
+ it 'the subgroup "Share with group lock" does not change' do
+ root_group.update!(share_with_group_lock: false)
+
+ expect(subgroup.reload.share_with_group_lock?).to be_falsey
+ end
+ end
+ end
+
+ # Note: Group transfers are not yet implemented
+ context 'when a group is transferred into a root group' do
+ context 'when the root group "Share with group lock" is enabled' do
+ let(:root_group) { create(:group, share_with_group_lock: true) }
+
+ context 'when the subgroup "Share with group lock" is enabled' do
+ let(:subgroup) { create(:group, share_with_group_lock: true )}
+
+ it 'the subgroup "Share with group lock" does not change' do
+ subgroup.parent = root_group
+ subgroup.save!
+
+ expect(subgroup.share_with_group_lock).to be_truthy
+ end
+ end
+
+ context 'when the subgroup "Share with group lock" is disabled' do
+ let(:subgroup) { create(:group)}
+
+ it 'the subgroup "Share with group lock" becomes enabled' do
+ subgroup.parent = root_group
+ subgroup.save!
+
+ expect(subgroup.share_with_group_lock).to be_truthy
+ end
+ end
+ end
+
+ context 'when the root group "Share with group lock" is disabled' do
+ let(:root_group) { create(:group) }
+
+ context 'when the subgroup "Share with group lock" is enabled' do
+ let(:subgroup) { create(:group, share_with_group_lock: true )}
+
+ it 'the subgroup "Share with group lock" does not change' do
+ subgroup.parent = root_group
+ subgroup.save!
+
+ expect(subgroup.share_with_group_lock).to be_truthy
+ end
+ end
+
+ context 'when the subgroup "Share with group lock" is disabled' do
+ let(:subgroup) { create(:group)}
+
+ it 'the subgroup "Share with group lock" does not change' do
+ subgroup.parent = root_group
+ subgroup.save!
+
+ expect(subgroup.share_with_group_lock).to be_falsey
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb
new file mode 100644
index 00000000000..ca13af4d73e
--- /dev/null
+++ b/spec/models/project_auto_devops_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe ProjectAutoDevops do
+ set(:project) { build(:project) }
+
+ it { is_expected.to belong_to(:project) }
+
+ it { is_expected.to respond_to(:created_at) }
+ it { is_expected.to respond_to(:updated_at) }
+
+ describe 'variables' do
+ let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: domain) }
+
+ context 'when domain is defined' do
+ let(:domain) { 'example.com' }
+
+ it 'returns AUTO_DEVOPS_DOMAIN' do
+ expect(auto_devops.variables).to include(
+ { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true })
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index 5b0f24ce306..d8972aff407 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -43,7 +43,7 @@ describe DroneCiService, :use_clean_rails_memory_store_caching do
let(:build_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" }
let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" }
- before(:each) do
+ before do
allow(drone).to receive_messages(
project_id: project.id,
project: project,
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 7614bb897e8..23db29cb541 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -36,7 +36,7 @@ describe HipchatService do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
- before(:each) do
+ before do
allow(hipchat).to receive_messages(
project_id: project.id,
project: project,
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 55b96a0c12e..537cdadd528 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -38,7 +38,8 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
'a' * 63 => true,
'a' * 64 => false,
'a.b' => false,
- 'a*b' => false
+ 'a*b' => false,
+ 'FOO' => true
}.each do |namespace, validity|
it "validates #{namespace} as #{validity ? 'valid' : 'invalid'}" do
subject.namespace = namespace
@@ -202,18 +203,13 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
describe '#predefined_variables' do
let(:kubeconfig) do
- config =
- YAML.load(File.read(expand_fixture_path('config/kubeconfig.yml')))
-
- config.dig('users', 0, 'user')['token'] =
- 'token'
-
+ config_file = expand_fixture_path('config/kubeconfig.yml')
+ config = YAML.load(File.read(config_file))
+ 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')
- config.dig('contexts', 0, 'context')['namespace'] =
- namespace
-
YAML.dump(config)
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 8f951605954..48fc77423ff 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -53,6 +53,7 @@ describe Project do
it { is_expected.to have_one(:import_data).class_name('ProjectImportData') }
it { is_expected.to have_one(:last_event).class_name('Event') }
it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) }
+ it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') }
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:builds) }
@@ -74,6 +75,7 @@ describe Project do
it { is_expected.to have_many(:forks).through(:forked_project_links) }
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) }
context 'after initialized' do
it "has a project_feature" do
@@ -90,22 +92,8 @@ describe Project do
project.team << [developer, :developer]
end
- describe '#members' do
- it 'includes members and exclude requesters' do
- member_user_ids = project.members.pluck(:user_id)
-
- expect(member_user_ids).to include(developer.id)
- expect(member_user_ids).not_to include(requester.id)
- end
- end
-
- describe '#requesters' do
- it 'does not include requesters' do
- requester_user_ids = project.requesters.pluck(:user_id)
-
- expect(requester_user_ids).to include(requester.id)
- expect(requester_user_ids).not_to include(developer.id)
- end
+ it_behaves_like 'members and requesters associations' do
+ let(:namespace) { project }
end
end
@@ -181,7 +169,7 @@ describe Project do
end
end
- context 'repository storages inclussion' do
+ context 'repository storages inclusion' do
let(:project2) { build(:project, repository_storage: 'missing') }
before do
@@ -485,7 +473,7 @@ describe Project do
describe 'last_activity' do
it 'alias last_activity to last_event' do
- last_event = create(:event, project: project)
+ last_event = create(:event, :closed, project: project)
expect(project.last_activity).to eq(last_event)
end
@@ -493,7 +481,7 @@ describe Project do
describe 'last_activity_date' do
it 'returns the creation date of the project\'s last event if present' do
- new_event = create(:event, project: project, created_at: Time.now)
+ new_event = create(:event, :closed, project: project, created_at: Time.now)
project.reload
expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
@@ -651,7 +639,7 @@ describe Project do
let(:ext_project) { create(:redmine_project) }
context 'on existing projects with no value for has_external_issue_tracker' do
- before(:each) do
+ before do
project.update_column(:has_external_issue_tracker, nil)
ext_project.update_column(:has_external_issue_tracker, nil)
end
@@ -1251,60 +1239,6 @@ describe Project do
end
end
- describe '#rename_repo' do
- let(:project) { create(:project, :repository) }
- let(:gitlab_shell) { Gitlab::Shell.new }
-
- before do
- # Project#gitlab_shell returns a new instance of Gitlab::Shell on every
- # call. This makes testing a bit easier.
- allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
- allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
- end
-
- it 'renames a repository' do
- stub_container_registry_config(enabled: false)
-
- expect(gitlab_shell).to receive(:mv_repository)
- .ordered
- .with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}")
- .and_return(true)
-
- expect(gitlab_shell).to receive(:mv_repository)
- .ordered
- .with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki")
- .and_return(true)
-
- expect_any_instance_of(SystemHooksService)
- .to receive(:execute_hooks_for)
- .with(project, :rename)
-
- expect_any_instance_of(Gitlab::UploadsTransfer)
- .to receive(:rename_project)
- .with('foo', project.path, project.namespace.full_path)
-
- expect(project).to receive(:expire_caches_before_rename)
-
- expect(project).to receive(:expires_full_path_cache)
-
- project.rename_repo
- end
-
- context 'container registry with images' do
- let(:container_repository) { create(:container_repository) }
-
- before do
- stub_container_registry_config(enabled: true)
- stub_container_registry_tags(repository: :any, tags: ['tag'])
- project.container_repositories << container_repository
- end
-
- subject { project.rename_repo }
-
- it { expect{subject}.to raise_error(StandardError) }
- end
- end
-
describe '#expire_caches_before_rename' do
let(:project) { create(:project, :repository) }
let(:repo) { double(:repo, exists?: true) }
@@ -1610,18 +1544,32 @@ describe Project do
it 'imports a project' do
expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original
- project.import_schedule
-
+ expect { project.import_schedule }.to change { project.import_jid }
expect(project.reload.import_status).to eq('finished')
end
end
describe 'project import state transitions' do
context 'state transition: [:started] => [:finished]' do
- let(:housekeeping_service) { spy }
+ let(:after_import_service) { spy(:after_import_service) }
+ let(:housekeeping_service) { spy(:housekeeping_service) }
before do
- allow(Projects::HousekeepingService).to receive(:new) { housekeeping_service }
+ allow(Projects::AfterImportService)
+ .to receive(:new) { after_import_service }
+
+ allow(after_import_service)
+ .to receive(:execute) { housekeeping_service.execute }
+
+ allow(Projects::HousekeepingService)
+ .to receive(:new) { housekeeping_service }
+ end
+
+ it 'resets project import_error' do
+ error_message = 'Some error'
+ mirror = create(:project_empty_repo, :import_started, import_error: error_message)
+
+ expect { mirror.import_finish }.to change { mirror.import_error }.from(error_message).to(nil)
end
it 'performs housekeeping when an import of a fresh project is completed' do
@@ -1629,6 +1577,7 @@ describe Project do
project.import_finish
+ expect(after_import_service).to have_received(:execute)
expect(housekeeping_service).to have_received(:execute)
end
@@ -1730,17 +1679,21 @@ describe Project do
end
describe '#add_import_job' do
+ let(:import_jid) { '123' }
+
context 'forked' do
let(:forked_project_link) { create(:forked_project_link, :forked_to_empty_project) }
let(:forked_from_project) { forked_project_link.forked_from_project }
let(:project) { forked_project_link.forked_to_project }
it 'schedules a RepositoryForkWorker job' do
- expect(RepositoryForkWorker).to receive(:perform_async)
- .with(project.id, forked_from_project.repository_storage_path,
- forked_from_project.disk_path, project.namespace.full_path)
+ expect(RepositoryForkWorker).to receive(:perform_async).with(
+ project.id,
+ forked_from_project.repository_storage_path,
+ forked_from_project.disk_path,
+ project.namespace.full_path).and_return(import_jid)
- project.add_import_job
+ expect(project.add_import_job).to eq(import_jid)
end
end
@@ -1748,9 +1701,8 @@ describe Project do
it 'schedules a RepositoryImportWorker job' do
project = create(:project, import_url: generate(:url))
- expect(RepositoryImportWorker).to receive(:perform_async).with(project.id)
-
- project.add_import_job
+ expect(RepositoryImportWorker).to receive(:perform_async).with(project.id).and_return(import_jid)
+ expect(project.add_import_job).to eq(import_jid)
end
end
end
@@ -1832,6 +1784,11 @@ describe Project do
describe '#change_head' do
let(:project) { create(:project, :repository) }
+ it 'returns error if branch does not exist' do
+ expect(project.change_head('unexisted-branch')).to be false
+ expect(project.errors.size).to eq(1)
+ end
+
it 'calls the before_change_head and after_change_head methods' do
expect(project.repository).to receive(:before_change_head)
expect(project.repository).to receive(:after_change_head)
@@ -2265,6 +2222,28 @@ describe Project do
end
end
+ describe '#pages_available?' do
+ let(:project) { create(:project, group: group) }
+
+ subject { project.pages_available? }
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ end
+
+ context 'when the project is in a top level namespace' do
+ let(:group) { create(:group) }
+
+ it { is_expected.to be(true) }
+ end
+
+ context 'when the project is in a subgroup' do
+ let(:group) { create(:group, :nested) }
+
+ it { is_expected.to be(false) }
+ end
+ end
+
describe '#remove_private_deploy_keys' do
let!(:project) { create(:project) }
@@ -2305,4 +2284,402 @@ describe Project do
end
end
end
+
+ describe '#remove_pages' do
+ let(:project) { create(:project) }
+ let(:namespace) { project.namespace }
+ let(:pages_path) { project.pages_path }
+
+ around do |example|
+ FileUtils.mkdir_p(pages_path)
+ begin
+ example.run
+ ensure
+ FileUtils.rm_rf(pages_path)
+ end
+ end
+
+ it 'removes the pages directory' do
+ expect_any_instance_of(Projects::UpdatePagesConfigurationService).to receive(:execute)
+ expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return(true)
+ expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, namespace.full_path, anything)
+
+ project.remove_pages
+ end
+
+ it 'is a no-op when there is no namespace' do
+ project.update_column(:namespace_id, nil)
+
+ expect_any_instance_of(Projects::UpdatePagesConfigurationService).not_to receive(:execute)
+ expect_any_instance_of(Gitlab::PagesTransfer).not_to receive(:rename_project)
+
+ project.remove_pages
+ end
+
+ it 'is run when the project is destroyed' do
+ expect(project).to receive(:remove_pages).and_call_original
+
+ project.destroy
+ end
+ end
+
+ describe '#forks_count' do
+ it 'returns the number of forks' do
+ project = build(:project)
+
+ allow(project.forks).to receive(:count).and_return(1)
+
+ expect(project.forks_count).to eq(1)
+ end
+ end
+
+ context 'legacy storage' do
+ let(:project) { create(:project, :repository) }
+ let(:gitlab_shell) { Gitlab::Shell.new }
+
+ before do
+ allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
+ end
+
+ describe '#base_dir' do
+ it 'returns base_dir based on namespace only' do
+ expect(project.base_dir).to eq(project.namespace.full_path)
+ end
+ end
+
+ describe '#disk_path' do
+ it 'returns disk_path based on namespace and project path' do
+ expect(project.disk_path).to eq("#{project.namespace.full_path}/#{project.path}")
+ end
+ end
+
+ describe '#ensure_storage_path_exists' do
+ it 'delegates to gitlab_shell to ensure namespace is created' do
+ expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, project.base_dir)
+
+ project.ensure_storage_path_exists
+ end
+ end
+
+ describe '#legacy_storage?' do
+ it 'returns true when storage_version is nil' do
+ project = build(:project)
+
+ expect(project.legacy_storage?).to be_truthy
+ end
+ end
+
+ describe '#rename_repo' do
+ before do
+ # Project#gitlab_shell returns a new instance of Gitlab::Shell on every
+ # call. This makes testing a bit easier.
+ allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
+ allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
+ end
+
+ it 'renames a repository' do
+ stub_container_registry_config(enabled: false)
+
+ expect(gitlab_shell).to receive(:mv_repository)
+ .ordered
+ .with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}")
+ .and_return(true)
+
+ expect(gitlab_shell).to receive(:mv_repository)
+ .ordered
+ .with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki")
+ .and_return(true)
+
+ expect_any_instance_of(SystemHooksService)
+ .to receive(:execute_hooks_for)
+ .with(project, :rename)
+
+ expect_any_instance_of(Gitlab::UploadsTransfer)
+ .to receive(:rename_project)
+ .with('foo', project.path, project.namespace.full_path)
+
+ expect(project).to receive(:expire_caches_before_rename)
+
+ expect(project).to receive(:expires_full_path_cache)
+
+ project.rename_repo
+ end
+
+ context 'container registry with images' do
+ let(:container_repository) { create(:container_repository) }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(repository: :any, tags: ['tag'])
+ project.container_repositories << container_repository
+ end
+
+ subject { project.rename_repo }
+
+ it { expect { subject }.to raise_error(StandardError) }
+ end
+ end
+
+ describe '#pages_path' do
+ it 'returns a path where pages are stored' do
+ expect(project.pages_path).to eq(File.join(Settings.pages.path, project.namespace.full_path, project.path))
+ end
+ end
+ end
+
+ context 'hashed storage' do
+ let(:project) { create(:project, :repository) }
+ let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:hash) { '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b' }
+
+ before do
+ stub_application_setting(hashed_storage_enabled: true)
+ allow(Digest::SHA2).to receive(:hexdigest) { hash }
+ allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
+ 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')
+ end
+ end
+
+ describe '#disk_path' do
+ it 'returns disk_path based on hash of project id' do
+ hashed_path = '@hashed/6b/86/6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b'
+
+ expect(project.disk_path).to eq(hashed_path)
+ end
+ end
+
+ describe '#ensure_storage_path_exists' do
+ it 'delegates to gitlab_shell to ensure namespace is created' do
+ expect(gitlab_shell).to receive(:add_namespace).with(project.repository_storage_path, '@hashed/6b/86')
+
+ project.ensure_storage_path_exists
+ end
+ end
+
+ describe '#rename_repo' do
+ before do
+ # Project#gitlab_shell returns a new instance of Gitlab::Shell on every
+ # call. This makes testing a bit easier.
+ allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
+ allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
+ end
+
+ it 'renames a repository' do
+ stub_container_registry_config(enabled: false)
+
+ expect(gitlab_shell).not_to receive(:mv_repository)
+
+ expect_any_instance_of(SystemHooksService)
+ .to receive(:execute_hooks_for)
+ .with(project, :rename)
+
+ expect_any_instance_of(Gitlab::UploadsTransfer)
+ .to receive(:rename_project)
+ .with('foo', project.path, project.namespace.full_path)
+
+ expect(project).to receive(:expire_caches_before_rename)
+
+ expect(project).to receive(:expires_full_path_cache)
+
+ project.rename_repo
+ end
+
+ context 'container registry with images' do
+ let(:container_repository) { create(:container_repository) }
+
+ before do
+ stub_container_registry_config(enabled: true)
+ stub_container_registry_tags(repository: :any, tags: ['tag'])
+ project.container_repositories << container_repository
+ end
+
+ subject { project.rename_repo }
+
+ it { expect { subject }.to raise_error(StandardError) }
+ end
+ end
+
+ describe '#pages_path' do
+ it 'returns a path where pages are stored' do
+ expect(project.pages_path).to eq(File.join(Settings.pages.path, project.namespace.full_path, project.path))
+ end
+ end
+ end
+
+ describe '#has_ci?' do
+ set(:project) { create(:project) }
+ let(:repository) { double }
+
+ before do
+ expect(project).to receive(:repository) { repository }
+ end
+
+ context 'when has .gitlab-ci.yml' do
+ before do
+ expect(repository).to receive(:gitlab_ci_yml) { 'content' }
+ end
+
+ it "CI is available" do
+ expect(project).to have_ci
+ end
+ end
+
+ context 'when there is no .gitlab-ci.yml' do
+ before do
+ expect(repository).to receive(:gitlab_ci_yml) { nil }
+ end
+
+ it "CI is not available" do
+ expect(project).not_to have_ci
+ end
+
+ context 'when auto devops is enabled' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it "CI is available" do
+ expect(project).to have_ci
+ end
+ end
+ end
+ end
+
+ describe '#auto_devops_enabled?' do
+ set(:project) { create(:project) }
+
+ subject { project.auto_devops_enabled? }
+
+ context 'when enabled in settings' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it 'auto devops is implicitly enabled' do
+ expect(project.auto_devops).to be_nil
+ expect(project).to be_auto_devops_enabled
+ end
+
+ context 'when explicitly enabled' do
+ before do
+ create(:project_auto_devops, project: project)
+ end
+
+ it "auto devops is enabled" do
+ expect(project).to be_auto_devops_enabled
+ end
+ end
+
+ context 'when explicitly disabled' do
+ before do
+ create(:project_auto_devops, project: project, enabled: false)
+ end
+
+ it "auto devops is disabled" do
+ expect(project).not_to be_auto_devops_enabled
+ end
+ end
+ end
+
+ context 'when disabled in settings' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ it 'auto devops is implicitly disabled' do
+ expect(project.auto_devops).to be_nil
+ expect(project).not_to be_auto_devops_enabled
+ end
+
+ context 'when explicitly enabled' do
+ before do
+ create(:project_auto_devops, project: project)
+ end
+
+ it "auto devops is enabled" do
+ expect(project).to be_auto_devops_enabled
+ end
+ end
+ end
+ end
+
+ describe '#has_auto_devops_implicitly_disabled?' do
+ set(:project) { create(:project) }
+
+ context 'when enabled in settings' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it 'does not have auto devops implicitly disabled' do
+ expect(project).not_to have_auto_devops_implicitly_disabled
+ end
+ end
+
+ context 'when disabled in settings' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ it 'auto devops is implicitly disabled' do
+ expect(project).to have_auto_devops_implicitly_disabled
+ end
+
+ context 'when explicitly disabled' do
+ before do
+ create(:project_auto_devops, project: project, enabled: false)
+ end
+
+ it 'does not have auto devops implicitly disabled' do
+ expect(project).not_to have_auto_devops_implicitly_disabled
+ end
+ end
+
+ context 'when explicitly enabled' do
+ before do
+ create(:project_auto_devops, project: project)
+ end
+
+ it 'does not have auto devops implicitly disabled' do
+ expect(project).not_to have_auto_devops_implicitly_disabled
+ end
+ end
+ end
+ end
+
+ context '#auto_devops_variables' do
+ set(:project) { create(:project) }
+
+ subject { project.auto_devops_variables }
+
+ context 'when enabled in settings' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ context 'when domain is empty' do
+ before do
+ create(:project_auto_devops, project: project, domain: nil)
+ end
+
+ it 'variables are empty' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when domain is configured' do
+ before do
+ create(:project_auto_devops, project: project, domain: 'example.com')
+ end
+
+ it "variables are not empty" do
+ is_expected.not_to be_empty
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/protectable_dropdown_spec.rb b/spec/models/protectable_dropdown_spec.rb
index 5c5dcd9f5c9..d4433a88a15 100644
--- a/spec/models/protectable_dropdown_spec.rb
+++ b/spec/models/protectable_dropdown_spec.rb
@@ -4,6 +4,13 @@ describe ProtectableDropdown do
let(:project) { create(:project, :repository) }
let(:subject) { described_class.new(project, :branches) }
+ describe 'initialize' do
+ it 'raises ArgumentError for invalid ref type' do
+ expect { described_class.new(double, :foo) }
+ .to raise_error(ArgumentError, "invalid ref type `foo`")
+ end
+ end
+
describe '#protectable_ref_names' do
before do
project.protected_branches.create(name: 'master')
diff --git a/spec/models/push_event_payload_spec.rb b/spec/models/push_event_payload_spec.rb
new file mode 100644
index 00000000000..a049ad35584
--- /dev/null
+++ b/spec/models/push_event_payload_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe PushEventPayload do
+ describe 'saving payloads' do
+ it 'does not allow commit messages longer than 70 characters' do
+ event = create(:push_event)
+ payload = build(:push_event_payload, event: event)
+
+ expect(payload).to be_valid
+
+ payload.commit_title = 'a' * 100
+
+ expect(payload).not_to be_valid
+ end
+ end
+end
diff --git a/spec/models/push_event_spec.rb b/spec/models/push_event_spec.rb
new file mode 100644
index 00000000000..532fb024261
--- /dev/null
+++ b/spec/models/push_event_spec.rb
@@ -0,0 +1,202 @@
+require 'spec_helper'
+
+describe PushEvent do
+ let(:payload) { PushEventPayload.new }
+
+ let(:event) do
+ event = described_class.new
+
+ allow(event).to receive(:push_event_payload).and_return(payload)
+
+ event
+ end
+
+ describe '.sti_name' do
+ it 'returns Event::PUSHED' do
+ expect(described_class.sti_name).to eq(Event::PUSHED)
+ end
+ end
+
+ describe '#push?' do
+ it 'returns true' do
+ expect(event).to be_push
+ end
+ end
+
+ describe '#push_with_commits?' do
+ it 'returns true when both the first and last commit are present' do
+ allow(event).to receive(:commit_from).and_return('123')
+ allow(event).to receive(:commit_to).and_return('456')
+
+ expect(event).to be_push_with_commits
+ end
+
+ it 'returns false when the first commit is missing' do
+ allow(event).to receive(:commit_to).and_return('456')
+
+ expect(event).not_to be_push_with_commits
+ end
+
+ it 'returns false when the last commit is missing' do
+ allow(event).to receive(:commit_from).and_return('123')
+
+ expect(event).not_to be_push_with_commits
+ end
+ end
+
+ describe '#tag?' do
+ it 'returns true when pushing to a tag' do
+ allow(payload).to receive(:tag?).and_return(true)
+
+ expect(event).to be_tag
+ end
+
+ it 'returns false when pushing to a branch' do
+ allow(payload).to receive(:tag?).and_return(false)
+
+ expect(event).not_to be_tag
+ end
+ end
+
+ describe '#branch?' do
+ it 'returns true when pushing to a branch' do
+ allow(payload).to receive(:branch?).and_return(true)
+
+ expect(event).to be_branch
+ end
+
+ it 'returns false when pushing to a tag' do
+ allow(payload).to receive(:branch?).and_return(false)
+
+ expect(event).not_to be_branch
+ end
+ end
+
+ describe '#valid_push?' do
+ it 'returns true if a ref exists' do
+ allow(payload).to receive(:ref).and_return('master')
+
+ expect(event).to be_valid_push
+ end
+
+ it 'returns false when no ref is present' do
+ expect(event).not_to be_valid_push
+ end
+ end
+
+ describe '#new_ref?' do
+ it 'returns true when pushing a new ref' do
+ allow(payload).to receive(:created?).and_return(true)
+
+ expect(event).to be_new_ref
+ end
+
+ it 'returns false when pushing to an existing ref' do
+ allow(payload).to receive(:created?).and_return(false)
+
+ expect(event).not_to be_new_ref
+ end
+ end
+
+ describe '#rm_ref?' do
+ it 'returns true when removing an existing ref' do
+ allow(payload).to receive(:removed?).and_return(true)
+
+ expect(event).to be_rm_ref
+ end
+
+ it 'returns false when pushing to an existing ref' do
+ allow(payload).to receive(:removed?).and_return(false)
+
+ expect(event).not_to be_rm_ref
+ end
+ end
+
+ describe '#commit_from' do
+ it 'returns the first commit SHA' do
+ allow(payload).to receive(:commit_from).and_return('123')
+
+ expect(event.commit_from).to eq('123')
+ end
+ end
+
+ describe '#commit_to' do
+ it 'returns the last commit SHA' do
+ allow(payload).to receive(:commit_to).and_return('123')
+
+ expect(event.commit_to).to eq('123')
+ end
+ end
+
+ describe '#ref_name' do
+ it 'returns the name of the ref' do
+ allow(payload).to receive(:ref).and_return('master')
+
+ expect(event.ref_name).to eq('master')
+ end
+ end
+
+ describe '#ref_type' do
+ it 'returns the type of the ref' do
+ allow(payload).to receive(:ref_type).and_return('branch')
+
+ expect(event.ref_type).to eq('branch')
+ end
+ end
+
+ describe '#branch_name' do
+ it 'returns the name of the branch' do
+ allow(payload).to receive(:ref).and_return('master')
+
+ expect(event.branch_name).to eq('master')
+ end
+ end
+
+ describe '#tag_name' do
+ it 'returns the name of the tag' do
+ allow(payload).to receive(:ref).and_return('1.2')
+
+ expect(event.tag_name).to eq('1.2')
+ end
+ end
+
+ describe '#commit_title' do
+ it 'returns the commit message' do
+ allow(payload).to receive(:commit_title).and_return('foo')
+
+ expect(event.commit_title).to eq('foo')
+ end
+ end
+
+ describe '#commit_id' do
+ it 'returns the SHA of the last commit if present' do
+ allow(event).to receive(:commit_to).and_return('123')
+
+ expect(event.commit_id).to eq('123')
+ end
+
+ it 'returns the SHA of the first commit if the last commit is not present' do
+ allow(event).to receive(:commit_to).and_return(nil)
+ allow(event).to receive(:commit_from).and_return('123')
+
+ expect(event.commit_id).to eq('123')
+ end
+ end
+
+ describe '#commits_count' do
+ it 'returns the number of commits' do
+ allow(payload).to receive(:commit_count).and_return(1)
+
+ expect(event.commits_count).to eq(1)
+ end
+ end
+
+ describe '#validate_push_action' do
+ it 'adds an error when the action is not PUSHED' do
+ event.action = Event::CREATED
+ event.validate_push_action
+
+ expect(event.errors.count).to eq(1)
+ end
+ end
+end
diff --git a/spec/models/redirect_route_spec.rb b/spec/models/redirect_route_spec.rb
index 80943877095..106ae59af29 100644
--- a/spec/models/redirect_route_spec.rb
+++ b/spec/models/redirect_route_spec.rb
@@ -20,8 +20,16 @@ describe RedirectRoute do
let!(:redirect4) { group.redirect_routes.create(path: 'gitlabb/test/foo/bar') }
let!(:redirect5) { group.redirect_routes.create(path: 'gitlabb/test/baz') }
- it 'returns correct routes' do
- expect(described_class.matching_path_and_descendants('gitlabb/test')).to match_array([redirect2, redirect3, redirect4, redirect5])
+ context 'when the redirect route matches with same casing' do
+ it 'returns correct routes' do
+ expect(described_class.matching_path_and_descendants('gitlabb/test')).to match_array([redirect2, redirect3, redirect4, redirect5])
+ end
+ end
+
+ context 'when the redirect route matches with different casing' do
+ it 'returns correct routes' do
+ expect(described_class.matching_path_and_descendants('GitLABB/test')).to match_array([redirect2, redirect3, redirect4, redirect5])
+ end
end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index cfa77648338..7065d467ec0 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -8,6 +8,7 @@ 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(:commit_options) do
author = repository.user_to_committer(user)
@@ -846,7 +847,7 @@ describe Repository, models: true do
expect do
repository.add_branch(user, 'new_feature', 'master')
- end.to raise_error(GitHooksService::PreReceiveError)
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
it 'does not create the branch' do
@@ -854,7 +855,7 @@ describe Repository, models: true do
expect do
repository.add_branch(user, 'new_feature', 'master')
- end.to raise_error(GitHooksService::PreReceiveError)
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
expect(repository.find_branch('new_feature')).to be_nil
end
end
@@ -884,8 +885,8 @@ describe Repository, models: true do
context 'when pre hooks were successful' do
it 'runs without errors' do
- expect_any_instance_of(GitHooksService).to receive(:execute)
- .with(user, project, old_rev, blank_sha, 'refs/heads/feature')
+ 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
@@ -905,7 +906,7 @@ describe Repository, models: true do
expect do
repository.rm_branch(user, 'feature')
- end.to raise_error(GitHooksService::PreReceiveError)
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
it 'does not delete the branch' do
@@ -913,7 +914,7 @@ describe Repository, models: true do
expect do
repository.rm_branch(user, 'feature')
- end.to raise_error(GitHooksService::PreReceiveError)
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
expect(repository.find_branch('feature')).not_to be_nil
end
end
@@ -922,26 +923,29 @@ describe Repository, models: true do
describe '#update_branch_with_hooks' do
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev
+ let(:updating_ref) { 'refs/heads/feature' }
+ let(:target_project) { project }
+ let(:target_repository) { target_project.repository }
context 'when pre hooks were successful' do
before do
- service = GitHooksService.new
- expect(GitHooksService).to receive(:new).and_return(service)
+ service = Gitlab::Git::HooksService.new
+ expect(Gitlab::Git::HooksService).to receive(:new).and_return(service)
expect(service).to receive(:execute)
- .with(user, project, old_rev, new_rev, 'refs/heads/feature')
+ .with(committer, target_repository.raw_repository, old_rev, new_rev, updating_ref)
.and_yield(service).and_return(true)
end
it 'runs without errors' do
expect do
- GitOperationService.new(user, repository).with_branch('feature') do
+ Gitlab::Git::OperationService.new(committer, 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 = GitOperationService.new(user, repository)
+ service = Gitlab::Git::OperationService.new(committer, repository.raw_repository)
expect(service).to receive(:update_autocrlf_option)
@@ -952,13 +956,65 @@ describe Repository, models: true do
it 'updates the head' do
expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev)
- GitOperationService.new(user, repository).with_branch('feature') do
+ Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do
new_rev
end
expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev)
end
end
+
+ context 'when target project does not have the commit' do
+ let(:target_project) { create(:project, :empty_repo) }
+ let(:old_rev) { Gitlab::Git::BLANK_SHA }
+ let(:new_rev) { project.commit('feature').sha }
+ let(:updating_ref) { 'refs/heads/master' }
+
+ it 'fetch_ref and create the branch' do
+ expect(target_project.repository.raw_repository).to receive(:fetch_ref)
+ .and_call_original
+
+ Gitlab::Git::OperationService.new(committer, target_repository.raw_repository)
+ .with_branch(
+ 'master',
+ start_repository: project.repository.raw_repository,
+ start_branch_name: 'feature') { new_rev }
+
+ expect(target_repository.branch_names).to contain_exactly('master')
+ end
+ end
+
+ context 'when target project already has the commit' do
+ let(:target_project) { create(:project, :repository) }
+
+ 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)
+ .with_branch('feature', start_repository: project.repository.raw_repository) { new_rev }
+ end
+ end
+ end
+
+ context 'when temporary ref failed to be created from other project' do
+ let(:target_project) { create(:project, :empty_repo) }
+
+ before do
+ expect(target_project.repository.raw_repository).to receive(:run_git)
+ end
+
+ it 'raises Rugged::ReferenceError' do
+ raise_reference_error = raise_error(Rugged::ReferenceError) do |err|
+ expect(err.cause).to be_nil
+ end
+
+ expect do
+ Gitlab::Git::OperationService.new(committer, target_project.repository.raw_repository)
+ .with_branch('feature',
+ start_repository: project.repository.raw_repository,
+ &:itself)
+ end.to raise_reference_error
+ end
end
context 'when the update adds more than one commit' do
@@ -975,7 +1031,7 @@ describe Repository, models: true do
repository.add_branch(user, branch, old_rev)
expect do
- GitOperationService.new(user, repository).with_branch(branch) do
+ Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do
new_rev
end
end.not_to raise_error
@@ -993,10 +1049,10 @@ 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
- GitOperationService.new(user, repository).with_branch(branch) do
+ Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do
new_rev
end
- end.to raise_error(Repository::CommitError)
+ end.to raise_error(Gitlab::Git::CommitError)
end
end
@@ -1005,10 +1061,10 @@ describe Repository, models: true do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
- GitOperationService.new(user, repository).with_branch('feature') do
+ Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do
new_rev
end
- end.to raise_error(GitHooksService::PreReceiveError)
+ end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
end
@@ -1023,10 +1079,9 @@ describe Repository, models: true do
expect(repository).not_to receive(:expire_emptiness_caches)
expect(repository).to receive(:expire_branches_cache)
- GitOperationService.new(user, repository)
- .with_branch('new-feature') do
- new_rev
- end
+ repository.with_branch(user, 'new-feature') do
+ new_rev
+ end
end
end
@@ -1083,7 +1138,7 @@ describe Repository, models: true do
describe 'when there are no branches' do
before do
- allow(repository).to receive(:branch_count).and_return(0)
+ allow(repository.raw_repository).to receive(:branch_count).and_return(0)
end
it { is_expected.to eq(false) }
@@ -1091,7 +1146,7 @@ describe Repository, models: true do
describe 'when there are branches' do
it 'returns true' do
- expect(repository).to receive(:branch_count).and_return(3)
+ expect(repository.raw_repository).to receive(:branch_count).and_return(3)
expect(subject).to eq(true)
end
@@ -1105,7 +1160,7 @@ describe Repository, models: true do
end
it 'sets autocrlf to :input' do
- GitOperationService.new(nil, repository).send(:update_autocrlf_option)
+ Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_autocrlf_option)
expect(repository.raw_repository.autocrlf).to eq(:input)
end
@@ -1120,7 +1175,7 @@ describe Repository, models: true do
expect(repository.raw_repository).not_to receive(:autocrlf=)
.with(:input)
- GitOperationService.new(nil, repository).send(:update_autocrlf_option)
+ Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_autocrlf_option)
end
end
end
@@ -1224,7 +1279,7 @@ describe Repository, models: true do
end
describe 'skip_merges option' do
- subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", limit: 100, skip_merges: true).map{ |k| k.id } }
+ subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", limit: 100, skip_merges: true).map { |k| k.id } }
it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') }
end
@@ -1323,8 +1378,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
- repository.cherry_pick(user, pickable_merge, 'improve/awesome')
+ cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome')
+ 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')
end
end
end
@@ -1703,15 +1761,15 @@ describe Repository, models: true do
describe '#update_ref' do
it 'can create a ref' do
- GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+ Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
expect(repository.find_branch('foobar')).not_to be_nil
end
it 'raises CommitError when the ref update fails' do
expect do
- GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
- end.to raise_error(Repository::CommitError)
+ Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+ end.to raise_error(Gitlab::Git::CommitError)
end
end
@@ -2014,23 +2072,23 @@ describe Repository, models: true do
end
end
- describe '#is_ancestor?' do
+ describe '#ancestor?' do
let(:commit) { repository.commit }
let(:ancestor) { commit.parents.first }
context 'with Gitaly enabled' do
it 'it is an ancestor' do
- expect(repository.is_ancestor?(ancestor.id, commit.id)).to eq(true)
+ expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
end
it 'it is not an ancestor' do
- expect(repository.is_ancestor?(commit.id, ancestor.id)).to eq(false)
+ expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
end
it 'returns false on nil-values' do
- expect(repository.is_ancestor?(nil, commit.id)).to eq(false)
- expect(repository.is_ancestor?(ancestor.id, nil)).to eq(false)
- expect(repository.is_ancestor?(nil, nil)).to eq(false)
+ expect(repository.ancestor?(nil, commit.id)).to eq(false)
+ expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
+ expect(repository.ancestor?(nil, nil)).to eq(false)
end
end
@@ -2041,17 +2099,17 @@ describe Repository, models: true do
end
it 'it is an ancestor' do
- expect(repository.is_ancestor?(ancestor.id, commit.id)).to eq(true)
+ expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
end
it 'it is not an ancestor' do
- expect(repository.is_ancestor?(commit.id, ancestor.id)).to eq(false)
+ expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
end
it 'returns false on nil-values' do
- expect(repository.is_ancestor?(nil, commit.id)).to eq(false)
- expect(repository.is_ancestor?(ancestor.id, nil)).to eq(false)
- expect(repository.is_ancestor?(nil, nil)).to eq(false)
+ expect(repository.ancestor?(nil, commit.id)).to eq(false)
+ expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
+ expect(repository.ancestor?(nil, nil)).to eq(false)
end
end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index bdacc60fb53..fece370c03f 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -145,45 +145,71 @@ describe Route do
describe '#delete_conflicting_redirects' do
context 'when a redirect route with the same path exists' do
- let!(:redirect1) { route.create_redirect(route.path) }
+ context 'when the redirect route has matching case' do
+ let!(:redirect1) { route.create_redirect(route.path) }
- it 'deletes the redirect' do
- route.delete_conflicting_redirects
- expect(route.conflicting_redirects).to be_empty
+ it 'deletes the redirect' do
+ expect do
+ route.delete_conflicting_redirects
+ end.to change { RedirectRoute.count }.by(-1)
+ end
+
+ context 'when redirect routes with paths descending from the route path exists' do
+ let!(:redirect2) { route.create_redirect("#{route.path}/foo") }
+ let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") }
+ let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") }
+ let!(:other_redirect) { route.create_redirect("other") }
+
+ it 'deletes all redirects with paths that descend from the route path' do
+ expect do
+ route.delete_conflicting_redirects
+ end.to change { RedirectRoute.count }.by(-4)
+ end
+ end
end
- context 'when redirect routes with paths descending from the route path exists' do
- let!(:redirect2) { route.create_redirect("#{route.path}/foo") }
- let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") }
- let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") }
- let!(:other_redirect) { route.create_redirect("other") }
+ context 'when the redirect route is differently cased' do
+ let!(:redirect1) { route.create_redirect(route.path.upcase) }
- it 'deletes all redirects with paths that descend from the route path' do
- route.delete_conflicting_redirects
- expect(route.conflicting_redirects).to be_empty
+ it 'deletes the redirect' do
+ expect do
+ route.delete_conflicting_redirects
+ end.to change { RedirectRoute.count }.by(-1)
end
end
end
end
describe '#conflicting_redirects' do
+ it 'returns an ActiveRecord::Relation' do
+ expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
+ end
+
context 'when a redirect route with the same path exists' do
- let!(:redirect1) { route.create_redirect(route.path) }
+ context 'when the redirect route has matching case' do
+ let!(:redirect1) { route.create_redirect(route.path) }
- it 'returns the redirect route' do
- expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
- expect(route.conflicting_redirects).to match_array([redirect1])
+ it 'returns the redirect route' do
+ expect(route.conflicting_redirects).to match_array([redirect1])
+ end
+
+ context 'when redirect routes with paths descending from the route path exists' do
+ let!(:redirect2) { route.create_redirect("#{route.path}/foo") }
+ let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") }
+ let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") }
+ let!(:other_redirect) { route.create_redirect("other") }
+
+ it 'returns the redirect routes' do
+ expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3, redirect4])
+ end
+ end
end
- context 'when redirect routes with paths descending from the route path exists' do
- let!(:redirect2) { route.create_redirect("#{route.path}/foo") }
- let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") }
- let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") }
- let!(:other_redirect) { route.create_redirect("other") }
+ context 'when the redirect route is differently cased' do
+ let!(:redirect1) { route.create_redirect(route.path.upcase) }
- it 'returns the redirect routes' do
- expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
- expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3, redirect4])
+ it 'returns the redirect route' do
+ expect(route.conflicting_redirects).to match_array([redirect1])
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 0103fb6040e..73a1e47149c 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -118,6 +118,17 @@ describe User do
expect(user).to validate_uniqueness_of(:username).case_insensitive
end
+
+ context 'when username is changed' do
+ let(:user) { build_stubbed(:user, username: 'old_path', namespace: build_stubbed(:namespace)) }
+
+ it 'validates move_dir is allowed for the namespace' do
+ expect(user.namespace).to receive(:any_project_has_container_registry_tags?).and_return(true)
+ user.username = 'new_path'
+ expect(user).to be_invalid
+ expect(user.errors.messages[:username].first).to match('cannot be changed if a personal project has container registry tags')
+ end
+ end
end
it { is_expected.to validate_presence_of(:projects_limit) }
@@ -705,6 +716,7 @@ describe User do
it "applies defaults to user" do
expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit)
expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group)
+ expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme)
expect(user.external).to be_falsey
end
end
@@ -715,6 +727,7 @@ describe User do
it "applies defaults to user" do
expect(user.projects_limit).to eq(123)
expect(user.can_create_group).to be_falsey
+ expect(user.theme_id).to eq(1)
end
end
@@ -778,6 +791,7 @@ describe User do
describe '.search' do
let!(:user) { create(:user, name: 'user', username: 'usern', email: 'email@gmail.com') }
let!(:user2) { create(:user, name: 'user name', username: 'username', email: 'someemail@gmail.com') }
+ let!(:user3) { create(:user, name: 'us', username: 'se', email: 'foo@gmail.com') }
describe 'name matching' do
it 'returns users with a matching name with exact match first' do
@@ -791,6 +805,14 @@ describe User do
it 'returns users with a matching name regardless of the casing' do
expect(described_class.search(user2.name.upcase)).to eq([user2])
end
+
+ it 'returns users with a exact matching name shorter than 3 chars' do
+ expect(described_class.search(user3.name)).to eq([user3])
+ end
+
+ it 'returns users with a exact matching name shorter than 3 chars regardless of the casing' do
+ expect(described_class.search(user3.name.upcase)).to eq([user3])
+ end
end
describe 'email matching' do
@@ -819,6 +841,14 @@ describe User do
it 'returns users with a matching username regardless of the casing' do
expect(described_class.search(user2.username.upcase)).to eq([user2])
end
+
+ it 'returns users with a exact matching username shorter than 3 chars' do
+ expect(described_class.search(user3.username)).to eq([user3])
+ end
+
+ it 'returns users with a exact matching username shorter than 3 chars regardless of the casing' do
+ expect(described_class.search(user3.username.upcase)).to eq([user3])
+ end
end
end
@@ -1280,7 +1310,7 @@ describe User do
let!(:project2) { create(:project, forked_from_project: project3) }
let!(:project3) { create(:project) }
let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) }
- let!(:push_event) { create(:event, :pushed, project: project1, target: project1, author: subject) }
+ let!(:push_event) { create(:push_event, project: project1, author: subject) }
let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) }
before do
@@ -1322,10 +1352,18 @@ describe User do
subject { create(:user) }
let!(:project1) { create(:project, :repository) }
let!(:project2) { create(:project, :repository, forked_from_project: project1) }
- let!(:push_data) do
- Gitlab::DataBuilder::Push.build_sample(project2, subject)
+
+ 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
- let!(:push_event) { create(:event, :pushed, project: project2, target: project1, author: subject, data: push_data) }
before do
project1.team << [subject, :master]
@@ -1337,7 +1375,7 @@ describe User do
end
it "excludes push event if branch has been deleted" do
- allow_any_instance_of(Repository).to receive(:branch_names).and_return(['foo'])
+ allow_any_instance_of(Repository).to receive(:branch_exists?).with('master').and_return(false)
expect(subject.recent_push).to eq(nil)
end
@@ -1352,8 +1390,13 @@ describe User do
expect(subject.recent_push(project1)).to eq(nil)
expect(subject.recent_push(project2)).to eq(push_event)
- push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject)
- push_event1 = create(:event, :pushed, project: project1, target: project1, author: subject, data: push_data1)
+ 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
end
@@ -1476,7 +1519,7 @@ describe User do
developer_project = create(:project) { |p| p.add_developer(user) }
master_project = create(:project) { |p| p.add_master(user) }
- expect(user.projects_where_can_admin_issues.to_a).to eq([master_project, developer_project, reporter_project])
+ expect(user.projects_where_can_admin_issues.to_a).to match_array([master_project, developer_project, reporter_project])
expect(user.can?(:admin_issue, master_project)).to eq(true)
expect(user.can?(:admin_issue, developer_project)).to eq(true)
expect(user.can?(:admin_issue, reporter_project)).to eq(true)
@@ -2000,4 +2043,145 @@ describe User do
expect(user.projects_limit_left).to eq(5)
end
end
+
+ describe '#ensure_namespace_correct' do
+ context 'for a new user' do
+ let(:user) { build(:user) }
+
+ it 'creates the namespace' do
+ expect(user.namespace).to be_nil
+ user.save!
+ expect(user.namespace).not_to be_nil
+ end
+ end
+
+ context 'for an existing user' do
+ let(:username) { 'foo' }
+ let(:user) { create(:user, username: username) }
+
+ context 'when the user is updated' do
+ context 'when the username is changed' do
+ let(:new_username) { 'bar' }
+
+ it 'changes the namespace (just to compare to when username is not changed)' do
+ expect do
+ user.update_attributes!(username: new_username)
+ end.to change { user.namespace.updated_at }
+ end
+
+ 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
+
+ context 'when there is a validation error (namespace name taken) while updating namespace' do
+ let!(:conflicting_namespace) { create(:group, name: new_username, path: 'quz') }
+
+ it 'causes the user save to fail' do
+ expect(user.update_attributes(username: new_username)).to be_falsey
+ expect(user.namespace.errors.messages[:name].first).to eq('has already been taken')
+ end
+
+ 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
+ end
+
+ context 'when the username is not changed' do
+ it 'does not change the namespace' do
+ expect do
+ user.update_attributes!(email: 'asdf@asdf.com')
+ end.not_to change { user.namespace.updated_at }
+ end
+ end
+ end
+ 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
+ end
+
+ it 'returns false for all syncable attributes but email if no syncable attributes are declared' do
+ expect(user.sync_attribute?(:name)).to be_falsey
+ expect(user.sync_attribute?(:email)).to be_truthy
+ expect(user.sync_attribute?(:location)).to be_falsey
+ end
+ end
+
+ 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
+ end
+
+ 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
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 40a222be24d..9ef8d117123 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -281,6 +281,12 @@ describe WikiPage do
@page.title = "Import-existing-repositories-into-GitLab"
expect(@page.title).to eq("Import existing repositories into GitLab")
end
+
+ it 'unescapes html' do
+ @page.title = 'foo &amp; bar'
+
+ expect(@page.title).to eq('foo & bar')
+ end
end
describe '#directory' do
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index b17a93e3fbe..b186a78e44a 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -24,8 +24,8 @@ describe GroupPolicy do
:admin_namespace,
:admin_group_member,
:change_visibility_level,
- :create_subgroup
- ]
+ (Gitlab::Database.postgresql? ? :create_subgroup : nil)
+ ].compact
end
before do
@@ -105,6 +105,8 @@ describe GroupPolicy do
let(:current_user) { owner }
it do
+ allow(Group).to receive(:supports_nested_groups?).and_return(true)
+
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
expect_allowed(*master_permissions)
@@ -116,6 +118,8 @@ describe GroupPolicy do
let(:current_user) { admin }
it do
+ allow(Group).to receive(:supports_nested_groups?).and_return(true)
+
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
expect_allowed(*master_permissions)
@@ -123,6 +127,36 @@ describe GroupPolicy do
end
end
+ describe 'when nested group support feature is disabled' do
+ before do
+ allow(Group).to receive(:supports_nested_groups?).and_return(false)
+ end
+
+ context 'admin' do
+ let(:current_user) { admin }
+
+ it 'allows every owner permission except creating subgroups' do
+ create_subgroup_permission = [:create_subgroup]
+ updated_owner_permissions = owner_permissions - create_subgroup_permission
+
+ expect_disallowed(*create_subgroup_permission)
+ expect_allowed(*updated_owner_permissions)
+ end
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it 'allows every owner permission except creating subgroups' do
+ create_subgroup_permission = [:create_subgroup]
+ updated_owner_permissions = owner_permissions - create_subgroup_permission
+
+ expect_disallowed(*create_subgroup_permission)
+ expect_allowed(*updated_owner_permissions)
+ end
+ end
+ end
+
describe 'private nested group use the highest access level from the group and inherited permissions', :nested_groups do
let(:nested_group) { create(:group, :private, parent: group) }
@@ -199,6 +233,8 @@ describe GroupPolicy do
let(:current_user) { owner }
it do
+ allow(Group).to receive(:supports_nested_groups?).and_return(true)
+
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
expect_allowed(*master_permissions)
@@ -206,4 +242,94 @@ describe GroupPolicy do
end
end
end
+
+ describe 'change_share_with_group_lock' do
+ context 'when the current_user owns the group' do
+ let(:current_user) { owner }
+
+ context 'when the group share_with_group_lock is enabled' do
+ let(:group) { create(:group, share_with_group_lock: true, parent: parent) }
+
+ context 'when the parent group share_with_group_lock is enabled' do
+ context 'when the group has a grandparent' do
+ let(:parent) { create(:group, share_with_group_lock: true, parent: grandparent) }
+
+ context 'when the grandparent share_with_group_lock is enabled' do
+ let(:grandparent) { create(:group, share_with_group_lock: true) }
+
+ context 'when the current_user owns the parent' do
+ before do
+ parent.add_owner(current_user)
+ end
+
+ context 'when the current_user owns the grandparent' do
+ before do
+ grandparent.add_owner(current_user)
+ end
+
+ it { expect_allowed(:change_share_with_group_lock) }
+ end
+
+ context 'when the current_user does not own the grandparent' do
+ it { expect_disallowed(:change_share_with_group_lock) }
+ end
+ end
+
+ context 'when the current_user does not own the parent' do
+ it { expect_disallowed(:change_share_with_group_lock) }
+ end
+ end
+
+ context 'when the grandparent share_with_group_lock is disabled' do
+ let(:grandparent) { create(:group) }
+
+ context 'when the current_user owns the parent' do
+ before do
+ parent.add_owner(current_user)
+ end
+
+ it { expect_allowed(:change_share_with_group_lock) }
+ end
+
+ context 'when the current_user does not own the parent' do
+ it { expect_disallowed(:change_share_with_group_lock) }
+ end
+ end
+ end
+
+ context 'when the group does not have a grandparent' do
+ let(:parent) { create(:group, share_with_group_lock: true) }
+
+ context 'when the current_user owns the parent' do
+ before do
+ parent.add_owner(current_user)
+ end
+
+ it { expect_allowed(:change_share_with_group_lock) }
+ end
+
+ context 'when the current_user does not own the parent' do
+ it { expect_disallowed(:change_share_with_group_lock) }
+ end
+ end
+ end
+
+ context 'when the parent group share_with_group_lock is disabled' do
+ let(:parent) { create(:group) }
+
+ it { expect_allowed(:change_share_with_group_lock) }
+ end
+ end
+
+ context 'when the group share_with_group_lock is disabled' do
+ it { expect_allowed(:change_share_with_group_lock) }
+ end
+ end
+
+ context 'when the current_user does not own the group' do
+ let(:current_user) { create(:user) }
+
+ it { expect_disallowed(:change_share_with_group_lock) }
+ end
+ end
end
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index a7a34ecac72..1a8001be6ab 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -100,4 +100,38 @@ describe Ci::BuildPresenter do
end
end
end
+
+ describe '#trigger_variables' do
+ let(:build) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
+ let(:trigger) { create(:ci_trigger, project: project) }
+ let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) }
+
+ context 'when variable is stored in ci_pipeline_variables' do
+ let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline) }
+
+ context 'when pipeline is triggered by trigger API' do
+ it 'returns variables' do
+ expect(presenter.trigger_variables).to eq([pipeline_variable.to_runner_variable])
+ end
+ end
+
+ context 'when pipeline is not triggered by trigger API' do
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'does not return variables' do
+ expect(presenter.trigger_variables).to eq([])
+ end
+ end
+ end
+
+ context 'when variable is stored in ci_trigger_requests.variables' do
+ before do
+ trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
+ end
+
+ it 'returns variables' do
+ expect(presenter.trigger_variables).to eq(trigger_request.user_variables)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 1dd9f3f6ddc..593068b8cd7 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -253,6 +253,10 @@ describe API::AwardEmoji do
expect(response).to have_http_status(404)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji/#{award_emoji.id}", user) }
+ end
end
context 'when the awardable is a Merge Request' do
@@ -269,6 +273,10 @@ describe API::AwardEmoji do
expect(response).to have_http_status(404)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/award_emoji/#{downvote.id}", user) }
+ end
end
context 'when the awardable is a Snippet' do
@@ -282,6 +290,10 @@ describe API::AwardEmoji do
expect(response).to have_http_status(204)
end.to change { snippet.award_emoji.count }.from(1).to(0)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) }
+ end
end
end
@@ -295,5 +307,9 @@ describe API::AwardEmoji do
expect(response).to have_http_status(204)
end.to change { note.award_emoji.count }.from(1).to(0)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji/#{rocket.id}", user) }
+ end
end
end
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 43b381c2219..f698d5dddb3 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -195,6 +195,10 @@ describe API::Boards do
expect(response).to have_http_status(204)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("#{base_url}/#{dev_list.id}", owner) }
+ end
end
end
end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 5a2e1b2cf2d..cc794fad3a7 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -75,6 +75,22 @@ describe API::Branches do
let(:route) { "/projects/#{project_id}/repository/branches/#{branch_name}" }
shared_examples_for 'repository branch' do
+ context 'HEAD request' do
+ it 'returns 204 No Content' do
+ head api(route, user)
+
+ expect(response).to have_gitlab_http_status(204)
+ expect(response.body).to be_empty
+ end
+
+ it 'returns 404 Not Found' do
+ head api("/projects/#{project_id}/repository/branches/unknown", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(response.body).to be_empty
+ end
+ end
+
it 'returns the repository branch' do
get api(route, current_user)
@@ -499,6 +515,10 @@ describe API::Branches do
expect(response).to have_gitlab_http_status(404)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/repository/branches/#{branch_name}", user) }
+ end
end
describe 'DELETE /projects/:id/repository/merged_branches' do
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index 67989689799..b043a333d33 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -171,6 +171,10 @@ describe API::BroadcastMessages do
expect(response).to have_http_status(403)
end
+ it_behaves_like '412 response' do
+ let(:request) { api("/broadcast_messages/#{message.id}", admin) }
+ end
+
it 'deletes the broadcast message for admins' do
expect do
delete api("/broadcast_messages/#{message.id}", admin)
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 8b62aa268d9..e4c73583545 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -16,8 +16,8 @@ describe API::CommitStatuses do
let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" }
context 'ci commit exists' do
- let!(:master) { project.pipelines.create(source: :push, sha: commit.id, ref: 'master') }
- let!(:develop) { project.pipelines.create(source: :push, sha: commit.id, ref: 'develop') }
+ let!(:master) { project.pipelines.create(source: :push, sha: commit.id, ref: 'master', protected: false) }
+ let!(:develop) { project.pipelines.create(source: :push, sha: commit.id, ref: 'develop', protected: false) }
context "reporter user" do
let(:statuses_id) { json_response.map { |status| status['id'] } }
@@ -44,8 +44,8 @@ describe API::CommitStatuses do
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id)
- json_response.sort_by!{ |status| status['id'] }
- expect(json_response.map{ |status| status['allow_failure'] }).to eq([true, false, false, false])
+ json_response.sort_by! { |status| status['id'] }
+ expect(json_response.map { |status| status['allow_failure'] }).to eq([true, false, false, false])
end
end
@@ -142,6 +142,9 @@ describe API::CommitStatuses do
expect(json_response['ref']).not_to be_empty
expect(json_response['target_url']).to be_nil
expect(json_response['description']).to be_nil
+ if status == 'failed'
+ expect(CommitStatus.find(json_response['id'])).to be_api_failure
+ end
end
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 992a6e8d76a..f663719d28c 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -16,11 +16,13 @@ describe API::Commits do
end
describe 'GET /projects/:id/repository/commits' do
- context 'authorized user' do
+ let(:route) { "/projects/#{project_id}/repository/commits" }
+
+ shared_examples_for 'project commits' do
it "returns project commits" do
commit = project.repository.commit
- get api("/projects/#{project_id}/repository/commits", user)
+ get api(route, current_user)
expect(response).to have_http_status(200)
expect(response).to match_response_schema('public_api/v4/commits')
@@ -32,7 +34,7 @@ describe API::Commits do
it 'include correct pagination headers' do
commit_count = project.repository.count_commits(ref: 'master').to_s
- get api("/projects/#{project_id}/repository/commits", user)
+ get api(route, current_user)
expect(response).to include_pagination_headers
expect(response.headers['X-Total']).to eq(commit_count)
@@ -40,140 +42,151 @@ describe API::Commits do
end
end
- context "unauthorized user" do
- it "does not return project commits" do
- get api("/projects/#{project_id}/repository/commits")
+ context 'when unauthenticated', 'and project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like 'project commits'
+ end
- expect(response).to have_http_status(404)
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
end
end
- context "since optional parameter" do
- it "returns project commits since provided parameter" do
- commits = project.repository.commits("master")
- after = commits.second.created_at
+ context 'when authenticated', 'as a master' do
+ let(:current_user) { user }
- get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
+ it_behaves_like 'project commits'
- expect(json_response.size).to eq 2
- expect(json_response.first["id"]).to eq(commits.first.id)
- expect(json_response.second["id"]).to eq(commits.second.id)
- end
+ context "since optional parameter" do
+ it "returns project commits since provided parameter" do
+ commits = project.repository.commits("master")
+ after = commits.second.created_at
- it 'include correct pagination headers' do
- commits = project.repository.commits("master")
- after = commits.second.created_at
- commit_count = project.repository.count_commits(ref: 'master', after: after).to_s
+ get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
- get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
+ expect(json_response.size).to eq 2
+ expect(json_response.first["id"]).to eq(commits.first.id)
+ expect(json_response.second["id"]).to eq(commits.second.id)
+ end
- expect(response).to include_pagination_headers
- expect(response.headers['X-Total']).to eq(commit_count)
- expect(response.headers['X-Page']).to eql('1')
+ it 'include correct pagination headers' do
+ commits = project.repository.commits("master")
+ after = commits.second.created_at
+ commit_count = project.repository.count_commits(ref: 'master', after: after).to_s
+
+ get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
+
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eql('1')
+ end
end
- end
- context "until optional parameter" do
- it "returns project commits until provided parameter" do
- commits = project.repository.commits("master")
- before = commits.second.created_at
+ context "until optional parameter" do
+ it "returns project commits until provided parameter" do
+ commits = project.repository.commits("master")
+ before = commits.second.created_at
- get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
- if commits.size >= 20
- expect(json_response.size).to eq(20)
- else
- expect(json_response.size).to eq(commits.size - 1)
- end
+ if commits.size >= 20
+ expect(json_response.size).to eq(20)
+ else
+ expect(json_response.size).to eq(commits.size - 1)
+ end
- expect(json_response.first["id"]).to eq(commits.second.id)
- expect(json_response.second["id"]).to eq(commits.third.id)
- end
+ expect(json_response.first["id"]).to eq(commits.second.id)
+ expect(json_response.second["id"]).to eq(commits.third.id)
+ end
- it 'include correct pagination headers' do
- commits = project.repository.commits("master")
- before = commits.second.created_at
- commit_count = project.repository.count_commits(ref: 'master', before: before).to_s
+ it 'include correct pagination headers' do
+ commits = project.repository.commits("master")
+ before = commits.second.created_at
+ commit_count = project.repository.count_commits(ref: 'master', before: before).to_s
- get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
- expect(response).to include_pagination_headers
- expect(response.headers['X-Total']).to eq(commit_count)
- expect(response.headers['X-Page']).to eql('1')
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eql('1')
+ end
end
- end
- context "invalid xmlschema date parameters" do
- it "returns an invalid parameter error message" do
- get api("/projects/#{project_id}/repository/commits?since=invalid-date", user)
+ context "invalid xmlschema date parameters" do
+ it "returns an invalid parameter error message" do
+ get api("/projects/#{project_id}/repository/commits?since=invalid-date", user)
- expect(response).to have_http_status(400)
- expect(json_response['error']).to eq('since is invalid')
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('since is invalid')
+ end
end
- end
- context "path optional parameter" do
- it "returns project commits matching provided path parameter" do
- path = 'files/ruby/popen.rb'
- commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
+ context "path optional parameter" do
+ it "returns project commits matching provided path parameter" do
+ path = 'files/ruby/popen.rb'
+ commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
- get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
+ get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
- expect(json_response.size).to eq(3)
- expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
- expect(response).to include_pagination_headers
- expect(response.headers['X-Total']).to eq(commit_count)
- end
+ expect(json_response.size).to eq(3)
+ expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ end
- it 'include correct pagination headers' do
- path = 'files/ruby/popen.rb'
- commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
+ it 'include correct pagination headers' do
+ path = 'files/ruby/popen.rb'
+ commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
- get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
+ get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
- expect(response).to include_pagination_headers
- expect(response.headers['X-Total']).to eq(commit_count)
- expect(response.headers['X-Page']).to eql('1')
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eql('1')
+ end
end
- end
- context 'with pagination params' do
- let(:page) { 1 }
- let(:per_page) { 5 }
- let(:ref_name) { 'master' }
- let!(:request) do
- get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user)
- end
+ context 'with pagination params' do
+ let(:page) { 1 }
+ let(:per_page) { 5 }
+ let(:ref_name) { 'master' }
+ let!(:request) do
+ get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user)
+ end
- it 'returns correct headers' do
- commit_count = project.repository.count_commits(ref: ref_name).to_s
+ it 'returns correct headers' do
+ commit_count = project.repository.count_commits(ref: ref_name).to_s
- expect(response).to include_pagination_headers
- expect(response.headers['X-Total']).to eq(commit_count)
- expect(response.headers['X-Page']).to eq('1')
- expect(response.headers['Link']).to match(/page=1&per_page=5/)
- expect(response.headers['Link']).to match(/page=2&per_page=5/)
- end
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eq('1')
+ expect(response.headers['Link']).to match(/page=1&per_page=5/)
+ expect(response.headers['Link']).to match(/page=2&per_page=5/)
+ end
- context 'viewing the first page' do
- it 'returns the first 5 commits' do
- commit = project.repository.commit
+ context 'viewing the first page' do
+ it 'returns the first 5 commits' do
+ commit = project.repository.commit
- expect(json_response.size).to eq(per_page)
- expect(json_response.first['id']).to eq(commit.id)
- expect(response.headers['X-Page']).to eq('1')
+ expect(json_response.size).to eq(per_page)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(response.headers['X-Page']).to eq('1')
+ end
end
- end
- context 'viewing the third page' do
- let(:page) { 3 }
+ context 'viewing the third page' do
+ let(:page) { 3 }
- it 'returns the third 5 commits' do
- commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first
+ it 'returns the third 5 commits' do
+ commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first
- expect(json_response.size).to eq(per_page)
- expect(json_response.first['id']).to eq(commit.id)
- expect(response.headers['X-Page']).to eq('3')
+ expect(json_response.size).to eq(per_page)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(response.headers['X-Page']).to eq('3')
+ end
end
end
end
@@ -552,7 +565,7 @@ describe API::Commits do
end
context 'when the ref has a pipeline' do
- let!(:pipeline) { project.pipelines.create(source: :push, ref: 'master', sha: commit.sha) }
+ let!(:pipeline) { project.pipelines.create(source: :push, ref: 'master', sha: commit.sha, protected: false) }
it 'includes a "created" status' do
get api(route, current_user)
@@ -660,6 +673,12 @@ describe API::Commits do
it_behaves_like 'ref diff'
end
end
+
+ context 'when binary diff are treated as text' do
+ let(:commit_id) { TestEnv::BRANCH_SHA['add-pdf-text-binary'] }
+
+ it_behaves_like 'ref diff'
+ end
end
end
@@ -791,7 +810,7 @@ describe API::Commits do
expect(response).to have_gitlab_http_status(201)
expect(response).to match_response_schema('public_api/v4/commit/basic')
expect(json_response['title']).to eq(commit.title)
- expect(json_response['message']).to eq(commit.message)
+ expect(json_response['message']).to eq(commit.cherry_pick_message(user))
expect(json_response['author_name']).to eq(commit.author_name)
expect(json_response['committer_name']).to eq(user.name)
end
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index d032d72de9c..684877c33c0 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -182,7 +182,7 @@ describe API::DeployKeys do
delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
expect(response).to have_http_status(204)
- end.to change{ project.deploy_keys.count }.by(-1)
+ end.to change { project.deploy_keys.count }.by(-1)
end
it 'returns 404 Not Found with invalid ID' do
@@ -190,6 +190,10 @@ describe API::DeployKeys do
expect(response).to have_http_status(404)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) }
+ end
end
describe 'POST /projects/:id/deploy_keys/:key_id/enable' do
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 87716c6fe3a..2361809e0e1 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -138,6 +138,10 @@ describe API::Environments do
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/environments/#{environment.id}", user) }
+ end
end
context 'a non member' do
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 48db964d782..a23d28994ce 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -59,6 +59,34 @@ describe API::Events do
expect(json_response.size).to eq(1)
end
+ context 'when the list of events includes push events' do
+ let(:event) do
+ create(:push_event, author: user, project: private_project)
+ end
+
+ let!(:payload) { create(:push_event_payload, event: event) }
+ let(:payload_hash) { json_response[0]['push_data'] }
+
+ before do
+ get api("/users/#{user.id}/events?action=pushed", user)
+ end
+
+ it 'responds with HTTP 200 OK' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'includes the push payload as a Hash' do
+ expect(payload_hash).to be_an_instance_of(Hash)
+ end
+
+ it 'includes the push payload details' do
+ expect(payload_hash['commit_count']).to eq(payload.commit_count)
+ expect(payload_hash['action']).to eq(payload.action)
+ expect(payload_hash['ref_type']).to eq(payload.ref_type)
+ expect(payload_hash['commit_to']).to eq(payload.commit_to)
+ end
+ end
+
context 'when there are multiple events from different projects' do
let(:second_note) { create(:note_on_issue, project: create(:project)) }
@@ -140,33 +168,28 @@ describe API::Events do
end
context 'when exists some events' do
+ let(:merge_request1) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') }
+ let(:merge_request2) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') }
+
before do
- create_event(note1)
- create_event(note2)
create_event(merge_request1)
end
- let(:note1) { create(:note_on_merge_request, project: private_project, author: user) }
- let(:note2) { create(:note_on_issue, project: private_project, author: user) }
- let(:merge_request1) { create(:merge_request, state: 'closed', author: user, assignee: user, source_project: private_project, title: 'Test') }
- let(:merge_request2) { create(:merge_request, state: 'closed', author: user, assignee: user, source_project: private_project, title: 'Test') }
-
it 'avoids N+1 queries' do
control_count = ActiveRecord::QueryRecorder.new do
- get api("/projects/#{private_project.id}/events", user)
+ get api("/projects/#{private_project.id}/events", user), target_type: :merge_request
end.count
create_event(merge_request2)
expect do
- get api("/projects/#{private_project.id}/events", user)
+ get api("/projects/#{private_project.id}/events", user), target_type: :merge_request
end.not_to exceed_query_limit(control_count)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response[0]).to include('target_type' => 'MergeRequest', 'target_id' => merge_request2.id)
- expect(json_response[1]).to include('target_type' => 'MergeRequest', 'target_id' => merge_request1.id)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |r| r['target_id'] }).to match_array([merge_request1.id, merge_request2.id])
end
def create_event(target)
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 55c998b13b8..114019441a3 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -33,6 +33,15 @@ describe API::Files do
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
end
+ it 'returns json when file has txt extension' do
+ file_path = "bar%2Fbranch-test.txt"
+
+ get api(route(file_path), current_user), params
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type).to eq('application/json')
+ end
+
it 'returns file by commit sha' do
# This file is deleted on HEAD
file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
@@ -116,6 +125,15 @@ describe API::Files do
expect(response).to have_http_status(200)
end
+ it 'returns raw file info for files with dots' do
+ url = route('.gitignore') + "/raw"
+ expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
+ get api(url, current_user), params
+
+ expect(response).to have_http_status(200)
+ end
+
it 'returns file by commit sha' do
# This file is deleted on HEAD
file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
@@ -206,7 +224,7 @@ describe API::Files do
it "returns a 400 if editor fails to create file" do
allow_any_instance_of(Repository).to receive(:create_file)
- .and_raise(Repository::CommitError, 'Cannot create file')
+ .and_raise(Gitlab::Git::CommitError, 'Cannot create file')
post api(route("any%2Etxt"), user), valid_params
@@ -220,6 +238,7 @@ describe API::Files do
post api(route("new_file_with_author%2Etxt"), user), valid_params
expect(response).to have_http_status(201)
+ expect(response.content_type).to eq('application/json')
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(author_email)
expect(last_commit.author_name).to eq(author_name)
@@ -320,7 +339,7 @@ describe API::Files do
end
it "returns a 400 if fails to delete file" do
- allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Repository::CommitError, 'Cannot delete file')
+ allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file')
delete api(route(file_path), user), valid_params
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index 402ea057cc5..93b9cf85c1d 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -88,7 +88,7 @@ describe API::GroupVariables do
it 'creates variable' do
expect do
post api("/groups/#{group.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true
- end.to change{group.variables.count}.by(1)
+ end.to change {group.variables.count}.by(1)
expect(response).to have_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
@@ -99,7 +99,7 @@ describe API::GroupVariables do
it 'creates variable with optional attributes' do
expect do
post api("/groups/#{group.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
- end.to change{group.variables.count}.by(1)
+ end.to change {group.variables.count}.by(1)
expect(response).to have_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
@@ -110,7 +110,7 @@ describe API::GroupVariables do
it 'does not allow to duplicate variable key' do
expect do
post api("/groups/#{group.id}/variables", user), key: variable.key, value: 'VALUE_2'
- end.to change{group.variables.count}.by(0)
+ end.to change {group.variables.count}.by(0)
expect(response).to have_http_status(400)
end
@@ -192,7 +192,7 @@ describe API::GroupVariables do
delete api("/groups/#{group.id}/variables/#{variable.key}", user)
expect(response).to have_http_status(204)
- end.to change{group.variables.count}.by(-1)
+ end.to change {group.variables.count}.by(-1)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
@@ -200,6 +200,10 @@ describe API::GroupVariables do
expect(response).to have_http_status(404)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/groups/#{group.id}/variables/#{variable.key}", user) }
+ end
end
context 'authorized user with invalid permissions' do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index eba1db15da6..42f0079e173 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -20,10 +20,15 @@ describe API::Groups do
describe "GET /groups" do
context "when unauthenticated" do
- it "returns authentication error" do
+ it "returns public groups" do
get api("/groups")
- expect(response).to have_http_status(401)
+ 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)
+ .to satisfy_one { |group| group['name'] == group1.name }
end
end
@@ -165,6 +170,18 @@ describe API::Groups do
end
describe "GET /groups/:id" do
+ context 'when unauthenticated' do
+ it 'returns 404 for a private group' do
+ get api("/groups/#{group2.id}")
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 200 for a public group' do
+ get api("/groups/#{group1.id}")
+ expect(response).to have_http_status(200)
+ end
+ end
+
context "when authenticated as user" do
it "returns one of user1's groups" do
project = create(:project, namespace: group2, path: 'Foo')
@@ -414,6 +431,30 @@ describe API::Groups do
expect(response).to have_http_status(403)
end
+
+ context 'as owner', :nested_groups do
+ before do
+ group2.add_owner(user1)
+ end
+
+ it 'can create subgroups' do
+ post api("/groups", user1), parent_id: group2.id, name: 'foo', path: 'foo'
+
+ expect(response).to have_http_status(201)
+ end
+ end
+
+ context 'as master', :nested_groups do
+ before do
+ group2.add_master(user1)
+ end
+
+ it 'cannot create subgroups' do
+ post api("/groups", user1), parent_id: group2.id, name: 'foo', path: 'foo'
+
+ expect(response).to have_http_status(403)
+ end
+ end
end
context "when authenticated as user with group permissions" do
@@ -427,6 +468,7 @@ describe API::Groups do
expect(json_response["name"]).to eq(group[:name])
expect(json_response["path"]).to eq(group[:path])
expect(json_response["request_access_enabled"]).to eq(group[:request_access_enabled])
+ expect(json_response["visibility"]).to eq(Gitlab::VisibilityLevel.string_level(Gitlab::CurrentSettings.current_application_settings.default_group_visibility))
end
it "creates a nested group", :nested_groups do
@@ -471,6 +513,10 @@ describe API::Groups do
expect(response).to have_http_status(204)
end
+ it_behaves_like '412 response' do
+ let(:request) { api("/groups/#{group1.id}", user1) }
+ end
+
it "does not remove a group if not an owner" do
user4 = create(:user)
group1.add_master(user4)
@@ -512,7 +558,7 @@ describe API::Groups do
let(:project) { create(:project) }
let(:project_path) { CGI.escape(project.full_path) }
- before(:each) do
+ before do
allow_any_instance_of(Projects::TransferService)
.to receive(:execute).and_return(true)
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 7a1bd76af7a..d4006fe71a2 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -56,7 +56,7 @@ describe API::Helpers do
end
def doorkeeper_guard_returns(value)
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ value }
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { value }
end
def error!(message, status, header)
@@ -161,7 +161,7 @@ describe API::Helpers do
describe "when authenticating using a user's private token" do
it "returns nil for an invalid token" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
- allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+ allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
expect(current_user).to be_nil
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 8a2de23716f..1274e66bb4c 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -5,13 +5,26 @@ describe API::Internal do
let(:key) { create(:key, user: user) }
let(:project) { create(:project, :repository) }
let(:secret_token) { Gitlab::Shell.secret_token }
+ let(:gl_repository) { "project-#{project.id}" }
+ let(:reference_counter) { double('ReferenceCounter') }
describe "GET /internal/check" do
it do
+ expect_any_instance_of(Redis).to receive(:ping).and_return('PONG')
+
get api("/internal/check"), secret_token: secret_token
expect(response).to have_http_status(200)
expect(json_response['api_version']).to eq(API::API.version)
+ expect(json_response['redis']).to be(true)
+ end
+
+ it 'returns false for field `redis` when redis is unavailable' do
+ expect_any_instance_of(Redis).to receive(:ping).and_raise(Errno::ENOENT)
+
+ get api("/internal/check"), secret_token: secret_token
+
+ expect(json_response['redis']).to be(false)
end
end
@@ -181,13 +194,12 @@ describe API::Internal do
describe "POST /internal/allowed", :clean_gitlab_redis_shared_state do
context "access granted" do
- before do
- project.team << [user, :developer]
- Timecop.freeze
+ around do |example|
+ Timecop.freeze { example.run }
end
- after do
- Timecop.return
+ before do
+ project.team << [user, :developer]
end
context 'with env passed as a JSON' do
@@ -661,6 +673,109 @@ describe API::Internal do
# end
# end
+ describe 'POST /internal/post_receive' do
+ let(:identifier) { 'key-123' }
+
+ let(:valid_params) do
+ {
+ gl_repository: gl_repository,
+ secret_token: secret_token,
+ identifier: identifier,
+ changes: changes
+ }
+ end
+
+ let(:changes) do
+ "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch"
+ end
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'enqueues a PostReceive worker job' do
+ expect(PostReceive).to receive(:perform_async)
+ .with(gl_repository, identifier, changes)
+
+ post api("/internal/post_receive"), valid_params
+ end
+
+ it 'decreases the reference counter and returns the result' do
+ expect(Gitlab::ReferenceCounter).to receive(:new).with(gl_repository)
+ .and_return(reference_counter)
+ expect(reference_counter).to receive(:decrease).and_return(true)
+
+ post api("/internal/post_receive"), valid_params
+
+ expect(json_response['reference_counter_decreased']).to be(true)
+ end
+
+ it 'returns link to create new merge request' do
+ post api("/internal/post_receive"), valid_params
+
+ expect(json_response['merge_request_urls']).to match [{
+ "branch_name" => "new_branch",
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
+ "new_merge_request" => true
+ }]
+ end
+
+ it 'returns empty array if printing_merge_request_link_enabled is false' do
+ project.update!(printing_merge_request_link_enabled: false)
+
+ post api("/internal/post_receive"), valid_params
+
+ expect(json_response['merge_request_urls']).to eq([])
+ end
+
+ context 'broadcast message exists' do
+ let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) }
+
+ it 'returns one broadcast message' do
+ post api("/internal/post_receive"), valid_params
+
+ expect(response).to have_http_status(200)
+ expect(json_response['broadcast_message']).to eq(broadcast_message.message)
+ end
+ end
+
+ context 'broadcast message does not exist' do
+ it 'returns empty string' do
+ post api("/internal/post_receive"), valid_params
+
+ expect(response).to have_http_status(200)
+ expect(json_response['broadcast_message']).to eq(nil)
+ end
+ end
+
+ context 'nil broadcast message' do
+ it 'returns empty string' do
+ allow(BroadcastMessage).to receive(:current).and_return(nil)
+
+ post api("/internal/post_receive"), valid_params
+
+ expect(response).to have_http_status(200)
+ expect(json_response['broadcast_message']).to eq(nil)
+ end
+ end
+ end
+
+ describe 'POST /internal/pre_receive' do
+ let(:valid_params) do
+ { gl_repository: gl_repository, secret_token: secret_token }
+ end
+
+ it 'decreases the reference counter and returns the result' do
+ expect(Gitlab::ReferenceCounter).to receive(:new).with(gl_repository)
+ .and_return(reference_counter)
+ expect(reference_counter).to receive(:increase).and_return(true)
+
+ post api("/internal/pre_receive"), valid_params
+
+ expect(json_response['reference_counter_increased']).to be(true)
+ end
+ end
+
def project_with_repo_path(path)
double().tap do |fake_project|
allow(fake_project).to receive_message_chain('repository.path_to_repo' => path)
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 7d120e4a234..1583d1c2435 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -138,6 +138,16 @@ describe API::Issues, :mailer do
expect(first_issue['id']).to eq(issue2.id)
end
+ it 'returns issues reacted by the authenticated user by the given emoji' do
+ issue2 = create(:issue, project: project, author: user, assignees: [user])
+ award_emoji = create(:award_emoji, awardable: issue2, user: user2, name: 'star')
+
+ get api('/issues', user2), my_reaction_emoji: award_emoji.name, scope: 'all'
+
+ expect_paginated_array_response(size: 1)
+ expect(first_issue['id']).to eq(issue2.id)
+ end
+
it 'returns issues matching given search string for title' do
get api("/issues", user), search: issue.title
@@ -509,6 +519,18 @@ describe API::Issues, :mailer do
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ get api("/projects/#{project.id}/issues", user)
+ end.count
+
+ create(:issue, author: user, project: project)
+
+ expect do
+ get api("/projects/#{project.id}/issues", user)
+ end.not_to exceed_query_limit(control_count)
+ end
+
it 'returns 404 when project does not exist' do
get api('/projects/1000/issues', non_member)
@@ -984,7 +1006,7 @@ describe API::Issues, :mailer do
describe 'POST /projects/:id/issues with spam filtering' do
before do
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
- allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
+ allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
end
let(:params) do
@@ -1114,7 +1136,7 @@ describe API::Issues, :mailer do
it "does not create a new project issue" do
allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
- allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
+ allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params
@@ -1304,6 +1326,10 @@ describe API::Issues, :mailer do
expect(response).to have_http_status(204)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}", owner) }
+ end
end
context 'when issue does not exist' do
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 548b612b5b0..8f213dbb597 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
-describe API::Jobs, :api do
- let(:project) do
+describe API::Jobs do
+ set(:project) do
create(:project, :repository, public_builds: false)
end
- let(:pipeline) do
+ set(:pipeline) do
create(:ci_empty_pipeline, project: project,
sha: project.commit.id,
ref: project.default_branch)
@@ -190,6 +190,84 @@ describe API::Jobs, :api do
end
end
+ describe 'GET /projects/:id/jobs/:job_id/artifacts/:artifact_path' do
+ context 'when job has artifacts' do
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ let(:artifact) do
+ 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif'
+ end
+
+ context 'when user is anonymous' do
+ let(:api_user) { nil }
+
+ context 'when project is public' do
+ it 'allows to access artifacts' do
+ project.update_column(:visibility_level,
+ Gitlab::VisibilityLevel::PUBLIC)
+ project.update_column(:public_builds, true)
+
+ get_artifact_file(artifact)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ context 'when project is public with builds access disabled' do
+ it 'rejects access to artifacts' do
+ project.update_column(:visibility_level,
+ Gitlab::VisibilityLevel::PUBLIC)
+ project.update_column(:public_builds, false)
+
+ get_artifact_file(artifact)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when project is private' do
+ it 'rejects access and hides existence of artifacts' do
+ project.update_column(:visibility_level,
+ Gitlab::VisibilityLevel::PRIVATE)
+ project.update_column(:public_builds, true)
+
+ get_artifact_file(artifact)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when user is authorized' do
+ it 'returns a specific artifact file for a valid path' do
+ expect(Gitlab::Workhorse)
+ .to receive(:send_artifacts_entry)
+ .and_call_original
+
+ get_artifact_file(artifact)
+
+ expect(response).to have_http_status(200)
+ expect(response.headers)
+ .to include('Content-Type' => 'application/json',
+ 'Gitlab-Workhorse-Send-Data' => /artifacts-entry/)
+ end
+ end
+ end
+
+ context 'when job does not have artifacts' do
+ it 'does not return job artifact file' do
+ get_artifact_file('some/artifact')
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ def get_artifact_file(artifact_path)
+ get api("/projects/#{project.id}/jobs/#{job.id}/" \
+ "artifacts/#{artifact_path}", api_user)
+ end
+ end
+
describe 'GET /projects/:id/jobs/:job_id/artifacts' do
before do
stub_artifacts_object_storage
@@ -256,8 +334,9 @@ describe API::Jobs, :api do
get_for_ref
end
- it 'gives 401' do
- expect(response).to have_http_status(401)
+ it 'does not find a resource in a private project' do
+ expect(project).to be_private
+ expect(response).to have_http_status(404)
end
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 5a4257d1009..b231fdea2a3 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -189,6 +189,11 @@ describe API::Labels do
delete api("/projects/#{project.id}/labels", user)
expect(response).to have_http_status(400)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/labels", user) }
+ let(:params) { { name: 'label1' } }
+ end
end
describe 'PUT /projects/:id/labels' do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 06aca698c91..d3bae8d2888 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -284,6 +284,10 @@ describe API::Members do
expect(response).to have_http_status(204)
end.to change { source.members.count }.by(-1)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master) }
+ end
end
it 'returns 404 if member does not exist' do
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index d1b22179888..d9da94d4713 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -14,7 +14,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions' do
it 'returns 200 for a valid merge request' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions", user)
- merge_request_diff = merge_request.merge_request_diffs.first
+ merge_request_diff = merge_request.merge_request_diffs.last
expect(response.status).to eq 200
expect(response).to include_pagination_headers
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 9eda6836ded..21d2c9644fb 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -31,7 +31,7 @@ describe API::MergeRequests do
it 'returns authentication error' do
get api('/merge_requests')
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -43,7 +43,7 @@ describe API::MergeRequests do
it 'returns an array of all merge requests' do
get api('/merge_requests', user), scope: :all
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |mr| mr['id'] })
@@ -56,7 +56,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), scope: :all
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |mr| mr['id'] })
@@ -68,7 +68,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(merge_request3.id)
@@ -79,7 +79,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), author_id: user2.id, scope: :all
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(merge_request3.id)
@@ -90,7 +90,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), assignee_id: user2.id, scope: :all
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(merge_request3.id)
@@ -101,7 +101,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2), scope: 'assigned-to-me'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(merge_request3.id)
@@ -112,7 +112,19 @@ describe API::MergeRequests do
get api('/merge_requests', user2), scope: 'created-by-me'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request3.id)
+ end
+
+ it 'returns merge requests reacted by the authenticated user by the given emoji' do
+ merge_request3 = create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ award_emoji = create(:award_emoji, awardable: merge_request3, user: user2, name: 'star')
+
+ get api('/merge_requests', user2), my_reaction_emoji: award_emoji.name, scope: 'all'
+
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(merge_request3.id)
@@ -125,7 +137,7 @@ describe API::MergeRequests do
it "returns authentication error" do
get api("/projects/#{project.id}/merge_requests")
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
end
end
@@ -145,7 +157,7 @@ describe API::MergeRequests do
it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
@@ -166,7 +178,7 @@ describe API::MergeRequests do
it "returns an array of all merge_requests using simple mode" do
get api("/projects/#{project.id}/merge_requests?view=simple", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
expect(json_response).to be_an Array
@@ -182,7 +194,7 @@ describe API::MergeRequests do
it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests?state", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
@@ -192,7 +204,7 @@ describe API::MergeRequests do
it "returns an array of open merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=opened", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -202,7 +214,7 @@ describe API::MergeRequests do
it "returns an array of closed merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=closed", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -212,7 +224,7 @@ describe API::MergeRequests do
it "returns an array of merged merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=merged", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -222,7 +234,7 @@ describe API::MergeRequests do
it 'returns merge_request by "iids" array' do
get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid]
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['title']).to eq merge_request_closed.title
@@ -232,14 +244,14 @@ describe API::MergeRequests do
it 'matches V4 response schema' do
get api("/projects/#{project.id}/merge_requests", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/merge_requests')
end
it 'returns an empty array if no issue matches milestone' do
get api("/projects/#{project.id}/merge_requests", user), milestone: '1.0.0'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -247,7 +259,7 @@ describe API::MergeRequests do
it 'returns an empty array if milestone does not exist' do
get api("/projects/#{project.id}/merge_requests", user), milestone: 'foo'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -262,7 +274,7 @@ describe API::MergeRequests do
it 'returns an array of merge requests matching state in milestone' do
get api("/projects/#{project.id}/merge_requests", user), milestone: '0.9', state: 'closed'
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(merge_request_closed.id)
@@ -271,7 +283,7 @@ describe API::MergeRequests do
it 'returns an array of labeled merge requests' do
get api("/projects/#{project.id}/merge_requests?labels=#{label.title}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label2.title, label.title])
@@ -280,7 +292,7 @@ describe API::MergeRequests do
it 'returns an array of labeled merge requests where all labels match' do
get api("/projects/#{project.id}/merge_requests?labels=#{label.title},foo,bar", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -288,7 +300,7 @@ describe API::MergeRequests do
it 'returns an empty array if no merge request matches labels' do
get api("/projects/#{project.id}/merge_requests?labels=foo,bar", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
@@ -307,7 +319,7 @@ describe API::MergeRequests do
get api("/projects/#{project.id}/merge_requests?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(mr2.id)
@@ -322,44 +334,44 @@ describe API::MergeRequests do
it "returns an array of merge_requests in ascending order" do
get api("/projects/#{project.id}/merge_requests?sort=asc", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
it "returns an array of merge_requests in descending order" do
get api("/projects/#{project.id}/merge_requests?sort=desc", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort.reverse)
end
it "returns an array of merge_requests ordered by updated_at" do
get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- response_dates = json_response.map{ |merge_request| merge_request['updated_at'] }
+ response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
expect(response_dates).to eq(response_dates.sort.reverse)
end
it "returns an array of merge_requests ordered by created_at" do
get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
end
@@ -370,7 +382,7 @@ describe API::MergeRequests do
it 'exposes known attributes' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(merge_request.id)
expect(json_response['iid']).to eq(merge_request.iid)
expect(json_response['project_id']).to eq(merge_request.project.id)
@@ -398,7 +410,7 @@ describe API::MergeRequests do
it "returns merge_request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq(merge_request.title)
expect(json_response['iid']).to eq(merge_request.iid)
expect(json_response['work_in_progress']).to eq(false)
@@ -409,13 +421,13 @@ describe API::MergeRequests do
it "returns a 404 error if merge_request_iid not found" do
get api("/projects/#{project.id}/merge_requests/999", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns a 404 error if merge_request `id` is used instead of iid" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
context 'Work in Progress' do
@@ -423,7 +435,7 @@ describe API::MergeRequests do
it "returns merge_request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['work_in_progress']).to eq(true)
end
end
@@ -434,7 +446,7 @@ describe API::MergeRequests do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user)
commit = merge_request.commits.first
- expect(response.status).to eq 200
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(merge_request.commits.size)
@@ -444,13 +456,13 @@ describe API::MergeRequests do
it 'returns a 404 when merge_request_iid not found' do
get api("/projects/#{project.id}/merge_requests/999/commits", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 when merge_request id is used instead of iid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -458,19 +470,19 @@ describe API::MergeRequests do
it 'returns the change information of the merge_request' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes", user)
- expect(response.status).to eq 200
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['changes'].size).to eq(merge_request.diffs.size)
end
it 'returns a 404 when merge_request_iid not found' do
get api("/projects/#{project.id}/merge_requests/999/changes", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns a 404 when merge_request id is used instead of iid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -485,7 +497,7 @@ describe API::MergeRequests do
labels: 'label, label2',
milestone_id: milestone.id
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
expect(json_response['labels']).to eq(%w(label label2))
expect(json_response['milestone']['id']).to eq(milestone.id)
@@ -495,25 +507,25 @@ describe API::MergeRequests do
it "returns 422 when source_branch equals target_branch" do
post api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
it "returns 400 when source_branch is missing" do
post api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", target_branch: "master", author: user
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns 400 when target_branch is missing" do
post api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", source_branch: "markdown", author: user
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it "returns 400 when title is missing" do
post api("/projects/#{project.id}/merge_requests", user),
target_branch: 'master', source_branch: 'markdown'
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
it 'allows special label names' do
@@ -523,7 +535,7 @@ describe API::MergeRequests do
target_branch: 'master',
author: user,
labels: 'label, label?, label&foo, ?, &'
- expect(response.status).to eq(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['labels']).to include 'label'
expect(json_response['labels']).to include 'label?'
expect(json_response['labels']).to include 'label&foo'
@@ -549,7 +561,7 @@ describe API::MergeRequests do
target_branch: 'master',
author: user
end.to change { MergeRequest.count }.by(0)
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
end
end
@@ -580,15 +592,17 @@ describe API::MergeRequests do
let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
- before :each do |each|
- fork_project.team << [user2, :reporter]
+ before do
+ fork_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),
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_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
expect(json_response['description']).to eq('Test description for Test merge_request')
end
@@ -599,7 +613,7 @@ describe API::MergeRequests do
expect(fork_project.forked_from_project).to eq(project)
post api("/projects/#{fork_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_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
end
@@ -613,25 +627,25 @@ describe API::MergeRequests do
author: user2,
target_project_id: project.id
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
it "returns 400 when source_branch is missing" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
- expect(response).to have_http_status(400)
+ 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),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
- expect(response).to have_http_status(400)
+ 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),
target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context 'when target_branch is specified' do
@@ -642,7 +656,7 @@ describe API::MergeRequests do
source_branch: 'markdown',
author: user,
target_project_id: fork_project.id
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
it 'returns 422 if targeting a different fork' do
@@ -652,14 +666,14 @@ describe API::MergeRequests do
source_branch: 'markdown',
author: user2,
target_project_id: unrelated_project.id
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
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
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
end
@@ -674,7 +688,7 @@ describe API::MergeRequests do
it "denies the deletion of the merge request" do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", developer)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -682,19 +696,23 @@ describe API::MergeRequests do
it "destroys the merge request owners can destroy" do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
- expect(response).to have_http_status(204)
+ expect(response).to have_gitlab_http_status(204)
end
it "returns 404 for an invalid merge request IID" do
delete api("/projects/#{project.id}/merge_requests/12345", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 if the merge request id is used instead of iid" do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) }
end
end
end
@@ -705,7 +723,7 @@ describe API::MergeRequests do
it "returns merge_request in case of success" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "returns 406 if branch can't be merged" do
@@ -714,21 +732,21 @@ describe API::MergeRequests do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
- expect(response).to have_http_status(406)
+ expect(response).to have_gitlab_http_status(406)
expect(json_response['message']).to eq('Branch cannot be merged')
end
it "returns 405 if merge_request is not open" do
merge_request.close
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
- expect(response).to have_http_status(405)
+ expect(response).to have_gitlab_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
it "returns 405 if merge_request is a work in progress" do
merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
- expect(response).to have_http_status(405)
+ expect(response).to have_gitlab_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
@@ -737,7 +755,7 @@ describe API::MergeRequests do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
- expect(response).to have_http_status(405)
+ expect(response).to have_gitlab_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
@@ -745,21 +763,21 @@ describe API::MergeRequests do
user2 = create(:user)
project.team << [user2, :reporter]
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user2)
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
end
it "returns 409 if the SHA parameter doesn't match" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha.reverse
- expect(response).to have_http_status(409)
+ expect(response).to have_gitlab_http_status(409)
expect(json_response['message']).to start_with('SHA does not match HEAD of source branch')
end
it "succeeds if the SHA parameter matches" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), sha: merge_request.diff_head_sha
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
end
it "enables merge when pipeline succeeds if the pipeline is active" do
@@ -768,7 +786,7 @@ describe API::MergeRequests do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), merge_when_pipeline_succeeds: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('Test')
expect(json_response['merge_when_pipeline_succeeds']).to eq(true)
end
@@ -780,7 +798,7 @@ describe API::MergeRequests do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), merge_when_pipeline_succeeds: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('Test')
expect(json_response['merge_when_pipeline_succeeds']).to eq(true)
end
@@ -788,13 +806,13 @@ describe API::MergeRequests do
it "returns 404 for an invalid merge request IID" do
put api("/projects/#{project.id}/merge_requests/12345/merge", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 if the merge request id is used instead of iid" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -803,39 +821,39 @@ describe API::MergeRequests do
it "returns merge_request" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: "close"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['state']).to eq('closed')
end
end
it "updates title and returns merge_request" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), title: "New title"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['title']).to eq('New title')
end
it "updates description and returns merge_request" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), description: "New description"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['description']).to eq('New description')
end
it "updates milestone_id and returns merge_request" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), milestone_id: milestone.id
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['milestone']['id']).to eq(milestone.id)
end
it "returns merge_request with renamed target_branch" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), target_branch: "wiki"
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['target_branch']).to eq('wiki')
end
it "returns merge_request that removes the source branch" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), remove_source_branch: true
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['force_remove_source_branch']).to be_truthy
end
@@ -856,7 +874,7 @@ describe API::MergeRequests do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', title: nil
merge_request.reload
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(merge_request.state).to eq('opened')
end
@@ -864,20 +882,20 @@ describe API::MergeRequests do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), state_event: 'close', target_branch: nil
merge_request.reload
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
expect(merge_request.state).to eq('opened')
end
it "returns 404 for an invalid merge request IID" do
put api("/projects/#{project.id}/merge_requests/12345", user), state_event: "close"
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 if the merge request id is used instead of iid" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close"
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -890,7 +908,7 @@ describe API::MergeRequests do
get api("/projects/#{project.id}/merge_requests/#{mr.iid}/closes_issues", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
@@ -900,7 +918,7 @@ describe API::MergeRequests do
it 'returns an empty array when there are no issues to be closed' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
@@ -916,7 +934,7 @@ describe API::MergeRequests do
get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
@@ -936,19 +954,19 @@ describe API::MergeRequests do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", guest)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
it "returns 404 for an invalid merge request IID" do
get api("/projects/#{project.id}/merge_requests/12345/closes_issues", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it "returns 404 if the merge request id is used instead of iid" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
end
@@ -956,26 +974,26 @@ describe API::MergeRequests do
it 'subscribes to a merge request' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", admin)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", user)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
it 'returns 404 if the merge request is not found' do
post api("/projects/#{project.id}/merge_requests/123/subscribe", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the merge request id is used instead of iid' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscribe", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 403 if user has no access to read code' do
@@ -984,7 +1002,7 @@ describe API::MergeRequests do
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/subscribe", guest)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
@@ -992,26 +1010,26 @@ describe API::MergeRequests do
it 'unsubscribes from a merge request' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", user)
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", admin)
- expect(response).to have_http_status(304)
+ expect(response).to have_gitlab_http_status(304)
end
it 'returns 404 if the merge request is not found' do
post api("/projects/#{project.id}/merge_requests/123/unsubscribe", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 if the merge request id is used instead of iid' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/unsubscribe", user)
- expect(response).to have_http_status(404)
+ expect(response).to have_gitlab_http_status(404)
end
it 'returns 403 if user has no access to read code' do
@@ -1020,7 +1038,7 @@ describe API::MergeRequests do
post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/unsubscribe", guest)
- expect(response).to have_http_status(403)
+ expect(response).to have_gitlab_http_status(403)
end
end
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 75e5062a99c..f5882c0c74a 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -390,6 +390,10 @@ describe API::Notes do
expect(response).to have_http_status(404)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", user) }
+ end
end
context 'when noteable is a Snippet' do
@@ -410,6 +414,10 @@ describe API::Notes do
expect(response).to have_http_status(404)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user) }
+ end
end
context 'when noteable is a Merge Request' do
@@ -430,6 +438,10 @@ describe API::Notes do
expect(response).to have_http_status(404)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes/#{merge_request_note.id}", user) }
+ end
end
end
end
diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb
index 9ff2b782b52..f650df57383 100644
--- a/spec/requests/api/pipeline_schedules_spec.rb
+++ b/spec/requests/api/pipeline_schedules_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::PipelineSchedules do
set(:developer) { create(:user) }
set(:user) { create(:user) }
- set(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository, public_builds: false) }
before do
project.add_developer(developer)
@@ -53,7 +53,7 @@ describe API::PipelineSchedules do
it 'returns matched pipeline schedules' do
get api("/projects/#{project.id}/pipeline_schedules", developer), scope: target
- expect(json_response.map{ |r| r['active'] }).to all(eq(active?(target)))
+ expect(json_response.map { |r| r['active'] }).to all(eq(active?(target)))
end
end
@@ -110,6 +110,18 @@ describe API::PipelineSchedules do
end
end
+ context 'authenticated user with insufficient permissions' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'does not return pipeline_schedules list' do
+ get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
context 'unauthenticated user' do
it 'does not return pipeline_schedules list' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
@@ -267,8 +279,7 @@ describe API::PipelineSchedules do
delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", master)
end.to change { project.pipeline_schedules.count }.by(-1)
- expect(response).to have_http_status(:accepted)
- expect(response).to match_response_schema('pipeline_schedule')
+ expect(response).to have_http_status(204)
end
it 'responds with 404 Not Found if requesting non-existing pipeline_schedule' do
@@ -276,6 +287,10 @@ describe API::PipelineSchedules do
expect(response).to have_http_status(:not_found)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", master) }
+ end
end
context 'authenticated user with invalid permissions' do
@@ -296,4 +311,150 @@ describe API::PipelineSchedules do
end
end
end
+
+ describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables' do
+ let(:params) { attributes_for(:ci_pipeline_schedule_variable) }
+
+ set(:pipeline_schedule) do
+ create(:ci_pipeline_schedule, project: project, owner: developer)
+ end
+
+ context 'authenticated user with valid permissions' do
+ context 'with required parameters' do
+ it 'creates pipeline_schedule_variable' do
+ expect do
+ post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer),
+ params
+ end.to change { pipeline_schedule.variables.count }.by(1)
+
+ expect(response).to have_http_status(:created)
+ expect(response).to match_response_schema('pipeline_schedule_variable')
+ expect(json_response['key']).to eq(params[:key])
+ expect(json_response['value']).to eq(params[:value])
+ end
+ end
+
+ context 'without required parameters' do
+ it 'does not create pipeline_schedule_variable' do
+ post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer)
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+
+ context 'when key has validation error' do
+ it 'does not create pipeline_schedule_variable' do
+ post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer),
+ params.merge('key' => '!?!?')
+
+ expect(response).to have_http_status(:bad_request)
+ expect(json_response['message']).to have_key('key')
+ end
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not create pipeline_schedule_variable' do
+ post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", user), params
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not create pipeline_schedule_variable' do
+ post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables"), params
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
+ set(:pipeline_schedule) do
+ create(:ci_pipeline_schedule, project: project, owner: developer)
+ end
+
+ let(:pipeline_schedule_variable) do
+ create(:ci_pipeline_schedule_variable, pipeline_schedule: pipeline_schedule)
+ end
+
+ context 'authenticated user with valid permissions' do
+ it 'updates pipeline_schedule_variable' do
+ put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer),
+ value: 'updated_value'
+
+ expect(response).to have_http_status(:ok)
+ expect(response).to match_response_schema('pipeline_schedule_variable')
+ expect(json_response['value']).to eq('updated_value')
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'does not update pipeline_schedule_variable' do
+ put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", user)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not update pipeline_schedule_variable' do
+ put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}")
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
+ let(:master) { create(:user) }
+
+ set(:pipeline_schedule) do
+ create(:ci_pipeline_schedule, project: project, owner: developer)
+ end
+
+ let!(:pipeline_schedule_variable) do
+ create(:ci_pipeline_schedule_variable, pipeline_schedule: pipeline_schedule)
+ end
+
+ before do
+ project.add_master(master)
+ end
+
+ context 'authenticated user with valid permissions' do
+ it 'deletes pipeline_schedule_variable' do
+ expect do
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", master)
+ end.to change { Ci::PipelineScheduleVariable.count }.by(-1)
+
+ expect(response).to have_http_status(:accepted)
+ expect(response).to match_response_schema('pipeline_schedule_variable')
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing pipeline_schedule_variable' do
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/____", master)
+
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: master) }
+
+ it 'does not delete pipeline_schedule_variable' do
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer)
+
+ expect(response).to have_http_status(:forbidden)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'does not delete pipeline_schedule_variable' do
+ delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}")
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 2829c243af3..ac3bab09c4c 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -212,5 +212,9 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(response).to have_http_status(404)
expect(WebHook.exists?(hook.id)).to be_truthy
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/hooks/#{hook.id}", user) }
+ end
end
end
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 2b541f5719e..db34149eb73 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -117,7 +117,7 @@ describe API::ProjectSnippets do
end
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
context 'when the snippet is private' do
@@ -179,7 +179,7 @@ describe API::ProjectSnippets do
end
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
context 'when the snippet is private' do
@@ -228,9 +228,6 @@ describe API::ProjectSnippets do
let(:snippet) { create(:project_snippet, author: admin) }
it 'deletes snippet' do
- admin = create(:admin)
- snippet = create(:project_snippet, author: admin)
-
delete api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin)
expect(response).to have_http_status(204)
@@ -242,6 +239,10 @@ describe API::ProjectSnippets do
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin) }
+ end
end
describe 'GET /projects/:project_id/snippets/:id/raw' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 9baac12821f..9602584f546 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -8,8 +8,8 @@ describe API::Projects do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
- let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:project2) { create(:project, path: 'project2', namespace: user.namespace) }
let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
let(:project_member) { create(:project_member, :developer, user: user3, project: project) }
let(:user4) { create(:user) }
@@ -414,6 +414,7 @@ describe API::Projects do
jobs_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
+ resolve_outdated_diff_discussions: false,
only_allow_merge_if_pipeline_succeeds: false,
request_access_enabled: true,
only_allow_merge_if_all_discussions_are_resolved: false,
@@ -477,20 +478,40 @@ describe API::Projects do
expect(json_response['avatar_url']).to eq("http://localhost/uploads/-/system/project/avatar/#{project_id}/banana_sample.gif")
end
+ it 'sets a project as allowing outdated diff discussions to automatically resolve' do
+ project = attributes_for(:project, resolve_outdated_diff_discussions: false)
+
+ post api('/projects', user), project
+
+ expect(json_response['resolve_outdated_diff_discussions']).to be_falsey
+ end
+
+ it 'sets a project as allowing outdated diff discussions to automatically resolve if resolve_outdated_diff_discussions' do
+ project = attributes_for(:project, resolve_outdated_diff_discussions: true)
+
+ post api('/projects', user), project
+
+ expect(json_response['resolve_outdated_diff_discussions']).to be_truthy
+ end
+
it 'sets a project as allowing merge even if build fails' do
- project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false })
+ project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false)
+
post api('/projects', user), project
+
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey
end
it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
- project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true })
+ project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: true)
+
post api('/projects', user), project
+
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy
end
it 'sets a project as allowing merge even if discussions are unresolved' do
- project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
+ project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: false)
post api('/projects', user), project
@@ -506,7 +527,7 @@ describe API::Projects do
end
it 'sets a project as allowing merge only if all discussions are resolved' do
- project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
+ project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: true)
post api('/projects', user), project
@@ -514,7 +535,7 @@ describe API::Projects do
end
it 'ignores import_url when it is nil' do
- project = attributes_for(:project, { import_url: nil })
+ project = attributes_for(:project, import_url: nil)
post api('/projects', user), project
@@ -574,7 +595,7 @@ describe API::Projects do
expect { post api("/projects/user/#{user.id}", admin), name: 'Foo Project' }.to change {Project.count}.by(1)
expect(response).to have_http_status(201)
- project = Project.first
+ project = Project.last
expect(project.name).to eq('Foo Project')
expect(project.path).to eq('foo-project')
@@ -585,7 +606,7 @@ describe API::Projects do
.to change { Project.count }.by(1)
expect(response).to have_http_status(201)
- project = Project.first
+ project = Project.last
expect(project.name).to eq('Foo Project')
expect(project.path).to eq('path-project-Foo')
@@ -642,20 +663,36 @@ describe API::Projects do
expect(json_response['visibility']).to eq('private')
end
+ it 'sets a project as allowing outdated diff discussions to automatically resolve' do
+ project = attributes_for(:project, resolve_outdated_diff_discussions: false)
+
+ post api("/projects/user/#{user.id}", admin), project
+
+ expect(json_response['resolve_outdated_diff_discussions']).to be_falsey
+ end
+
+ it 'sets a project as allowing outdated diff discussions to automatically resolve' do
+ project = attributes_for(:project, resolve_outdated_diff_discussions: true)
+
+ post api("/projects/user/#{user.id}", admin), project
+
+ expect(json_response['resolve_outdated_diff_discussions']).to be_truthy
+ end
+
it 'sets a project as allowing merge even if build fails' do
- project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false })
+ project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: false)
post api("/projects/user/#{user.id}", admin), project
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_falsey
end
- it 'sets a project as allowing merge only if merge_when_pipeline_succeeds' do
- project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: true })
+ it 'sets a project as allowing merge only if pipeline succeeds' do
+ project = attributes_for(:project, only_allow_merge_if_pipeline_succeeds: true)
post api("/projects/user/#{user.id}", admin), project
expect(json_response['only_allow_merge_if_pipeline_succeeds']).to be_truthy
end
it 'sets a project as allowing merge even if discussions are unresolved' do
- project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false })
+ project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: false)
post api("/projects/user/#{user.id}", admin), project
@@ -663,7 +700,7 @@ describe API::Projects do
end
it 'sets a project as allowing merge only if all discussions are resolved' do
- project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
+ project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: true)
post api("/projects/user/#{user.id}", admin), project
@@ -732,6 +769,7 @@ describe API::Projects do
expect(json_response['wiki_enabled']).to be_present
expect(json_response['jobs_enabled']).to be_present
expect(json_response['snippets_enabled']).to be_present
+ expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions)
expect(json_response['container_registry_enabled']).to be_present
expect(json_response['created_at']).to be_present
expect(json_response['last_activity_at']).to be_present
@@ -1029,6 +1067,10 @@ describe API::Projects do
delete api("/projects/#{project.id}/snippets/1234", user)
expect(response).to have_http_status(404)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/snippets/#{snippet.id}", user) }
+ end
end
describe 'GET /projects/:id/snippets/:snippet_id/raw' do
@@ -1065,6 +1107,14 @@ describe API::Projects do
expect(project_fork_target.forked?).to be_truthy
end
+ it 'refreshes the forks count cachce' do
+ expect(project_fork_source.forks_count).to be_zero
+
+ post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+
+ expect(project_fork_source.forks_count).to eq(1)
+ end
+
it 'fails if forked_from project which does not exist' do
post api("/projects/#{project_fork_target.id}/fork/9999", admin)
expect(response).to have_http_status(404)
@@ -1096,23 +1146,31 @@ describe API::Projects do
project_fork_target.group.add_developer user2
end
- it 'is forbidden to non-owner users' do
- delete api("/projects/#{project_fork_target.id}/fork", user2)
- expect(response).to have_http_status(403)
- end
+ context 'for a forked project' do
+ before do
+ post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project).not_to be_nil
+ expect(project_fork_target.forked?).to be_truthy
+ end
- it 'makes forked project unforked' do
- post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
- project_fork_target.reload
- expect(project_fork_target.forked_from_project).not_to be_nil
- expect(project_fork_target.forked?).to be_truthy
+ it 'makes forked project unforked' do
+ delete api("/projects/#{project_fork_target.id}/fork", admin)
- delete api("/projects/#{project_fork_target.id}/fork", admin)
+ expect(response).to have_http_status(204)
+ project_fork_target.reload
+ expect(project_fork_target.forked_from_project).to be_nil
+ expect(project_fork_target.forked?).not_to be_truthy
+ end
- expect(response).to have_http_status(204)
- project_fork_target.reload
- expect(project_fork_target.forked_from_project).to be_nil
- expect(project_fork_target.forked?).not_to be_truthy
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project_fork_target.id}/fork", admin) }
+ end
+ end
+
+ it 'is forbidden to non-owner users' do
+ delete api("/projects/#{project_fork_target.id}/fork", user2)
+ expect(response).to have_http_status(403)
end
it 'is idempotent if not forked' do
@@ -1180,14 +1238,23 @@ describe API::Projects do
end
describe 'DELETE /projects/:id/share/:group_id' do
- it 'returns 204 when deleting a group share' do
- group = create(:group, :public)
- create(:project_group_link, group: group, project: project)
+ context 'for a valid group' do
+ let(:group) { create(:group, :public) }
- delete api("/projects/#{project.id}/share/#{group.id}", user)
+ before do
+ create(:project_group_link, group: group, project: project)
+ end
- expect(response).to have_http_status(204)
- expect(project.project_group_links).to be_empty
+ it 'returns 204 when deleting a group share' do
+ delete api("/projects/#{project.id}/share/#{group.id}", user)
+
+ expect(response).to have_http_status(204)
+ expect(project.project_group_links).to be_empty
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/share/#{group.id}", user) }
+ end
end
it 'returns a 400 when group id is not an integer' do
@@ -1511,6 +1578,11 @@ describe API::Projects do
expect(json_response['message']).to eql('202 Accepted')
end
+ it_behaves_like '412 response' do
+ let(:success_status) { 202 }
+ let(:request) { api("/projects/#{project.id}", user) }
+ end
+
it 'does not remove a project if not an owner' do
user3 = create(:user)
project.team << [user3, :developer]
@@ -1541,6 +1613,11 @@ describe API::Projects do
delete api('/projects/1328', admin)
expect(response).to have_http_status(404)
end
+
+ it_behaves_like '412 response' do
+ let(:success_status) { 202 }
+ let(:request) { api("/projects/#{project.id}", admin) }
+ end
end
end
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index e4f9c47fb33..07d7f96bd70 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -96,7 +96,7 @@ describe API::ProtectedBranches do
describe 'POST /projects/:id/protected_branches' do
let(:branch_name) { 'new_branch' }
- context 'when authenticated as a master' do
+ context 'when authenticated as a master' do
before do
project.add_master(user)
end
@@ -213,6 +213,10 @@ describe API::ProtectedBranches do
expect(response).to have_gitlab_http_status(204)
end
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/protected_branches/#{branch_name}", user) }
+ end
+
it "returns 404 if branch does not exist" do
delete api("/projects/#{project.id}/protected_branches/barfoo", user)
@@ -221,7 +225,7 @@ describe API::ProtectedBranches do
context 'when branch has a wildcard in its name' do
let(:protected_name) { 'feature*' }
-
+
it "unprotects a wildcard branch" do
delete api("/projects/#{project.id}/protected_branches/#{branch_name}", user)
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index b439fb7e23a..25924559952 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -149,6 +149,11 @@ describe API::Runner do
expect(response).to have_http_status 204
expect(Ci::Runner.count).to eq(0)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api('/runners') }
+ let(:params) { { token: runner.token } }
+ end
end
end
@@ -553,17 +558,36 @@ describe API::Runner do
{ 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false }]
end
+ let(:trigger) { create(:ci_trigger, project: project) }
+ let!(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, builds: [job], trigger: trigger) }
+
before do
- trigger = create(:ci_trigger, project: project)
- create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [job], trigger: trigger)
project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
end
- it 'returns variables for triggers' do
- request_job
+ shared_examples 'expected variables behavior' do
+ it 'returns variables for triggers' do
+ request_job
- expect(response).to have_http_status(201)
- expect(json_response['variables']).to include(*expected_variables)
+ expect(response).to have_http_status(201)
+ expect(json_response['variables']).to include(*expected_variables)
+ end
+ end
+
+ context 'when variables are stored in trigger_request' do
+ before do
+ trigger_request.update_attribute(:variables, { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } )
+ end
+
+ it_behaves_like 'expected variables behavior'
+ end
+
+ context 'when variables are stored in pipeline_variables' do
+ before do
+ create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1')
+ end
+
+ it_behaves_like 'expected variables behavior'
end
end
@@ -622,13 +646,34 @@ describe API::Runner do
it 'mark job as succeeded' do
update_job(state: 'success')
- expect(job.reload.status).to eq 'success'
+ job.reload
+ expect(job).to be_success
end
it 'mark job as failed' do
update_job(state: 'failed')
- expect(job.reload.status).to eq 'failed'
+ job.reload
+ expect(job).to be_failed
+ expect(job).to be_unknown_failure
+ end
+
+ context 'when failure_reason is script_failure' do
+ before do
+ update_job(state: 'failed', failure_reason: 'script_failure')
+ job.reload
+ end
+
+ it { expect(job).to be_script_failure }
+ end
+
+ context 'when failure_reason is runner_system_failure' do
+ before do
+ update_job(state: 'failed', failure_reason: 'runner_system_failure')
+ job.reload
+ end
+
+ it { expect(job).to be_runner_system_failure }
end
end
@@ -684,7 +729,7 @@ describe API::Runner do
end
context 'when job has been updated recently' do
- it { expect{ patch_the_trace }.not_to change { job.updated_at }}
+ it { expect { patch_the_trace }.not_to change { job.updated_at }}
it "changes the job's trace" do
patch_the_trace
@@ -693,7 +738,7 @@ describe API::Runner do
end
context 'when Runner makes a force-patch' do
- it { expect{ force_patch_the_trace }.not_to change { job.updated_at }}
+ it { expect { force_patch_the_trace }.not_to change { job.updated_at }}
it "doesn't change the build.trace" do
force_patch_the_trace
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 3a95db030d4..67907579225 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -36,7 +36,7 @@ describe API::Runners do
it 'returns user available runners' do
get api('/runners', user)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any? { |r| r['is_shared'] }
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
@@ -46,7 +46,7 @@ describe API::Runners do
it 'filters runners by scope' do
get api('/runners?scope=active', user)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any? { |r| r['is_shared'] }
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
@@ -74,7 +74,7 @@ describe API::Runners do
it 'returns all runners' do
get api('/runners/all', admin)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any? { |r| r['is_shared'] }
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
@@ -93,7 +93,7 @@ describe API::Runners do
it 'filters runners by scope' do
get api('/runners/all?scope=specific', admin)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any? { |r| r['is_shared'] }
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
@@ -191,7 +191,8 @@ describe API::Runners do
active: !active,
tag_list: ['ruby2.1', 'pgsql', 'mysql'],
run_untagged: 'false',
- locked: 'true')
+ locked: 'true',
+ access_level: 'ref_protected')
shared_runner.reload
expect(response).to have_http_status(200)
@@ -200,6 +201,7 @@ describe API::Runners do
expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
expect(shared_runner.run_untagged?).to be(false)
expect(shared_runner.locked?).to be(true)
+ expect(shared_runner.ref_protected?).to be_truthy
expect(shared_runner.ensure_runner_queue_value)
.not_to eq(runner_queue_value)
end
@@ -277,7 +279,11 @@ describe API::Runners do
delete api("/runners/#{shared_runner.id}", admin)
expect(response).to have_http_status(204)
- end.to change{ Ci::Runner.shared.count }.by(-1)
+ end.to change { Ci::Runner.shared.count }.by(-1)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/runners/#{shared_runner.id}", admin) }
end
end
@@ -287,7 +293,7 @@ describe API::Runners do
delete api("/runners/#{unused_specific_runner.id}", admin)
expect(response).to have_http_status(204)
- end.to change{ Ci::Runner.specific.count }.by(-1)
+ end.to change { Ci::Runner.specific.count }.by(-1)
end
it 'deletes used runner' do
@@ -295,7 +301,7 @@ describe API::Runners do
delete api("/runners/#{specific_runner.id}", admin)
expect(response).to have_http_status(204)
- end.to change{ Ci::Runner.specific.count }.by(-1)
+ end.to change { Ci::Runner.specific.count }.by(-1)
end
end
@@ -330,7 +336,11 @@ describe API::Runners do
delete api("/runners/#{specific_runner.id}", user)
expect(response).to have_http_status(204)
- end.to change{ Ci::Runner.specific.count }.by(-1)
+ end.to change { Ci::Runner.specific.count }.by(-1)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/runners/#{specific_runner.id}", user) }
end
end
end
@@ -349,7 +359,7 @@ describe API::Runners do
it "returns project's runners" do
get api("/projects/#{project.id}/runners", user)
- shared = json_response.any?{ |r| r['is_shared'] }
+ shared = json_response.any? { |r| r['is_shared'] }
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
@@ -385,14 +395,14 @@ describe API::Runners do
it 'enables specific runner' do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
- end.to change{ project.runners.count }.by(+1)
+ end.to change { project.runners.count }.by(+1)
expect(response).to have_http_status(201)
end
it 'avoids changes when enabling already enabled runner' do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
- end.to change{ project.runners.count }.by(0)
+ end.to change { project.runners.count }.by(0)
expect(response).to have_http_status(409)
end
@@ -401,7 +411,7 @@ describe API::Runners do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
- end.to change{ project.runners.count }.by(0)
+ end.to change { project.runners.count }.by(0)
expect(response).to have_http_status(403)
end
@@ -416,7 +426,7 @@ describe API::Runners do
it 'enables any specific runner' do
expect do
post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id
- end.to change{ project.runners.count }.by(+1)
+ end.to change { project.runners.count }.by(+1)
expect(response).to have_http_status(201)
end
end
@@ -461,7 +471,11 @@ describe API::Runners do
delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
expect(response).to have_http_status(204)
- end.to change{ project.runners.count }.by(-1)
+ end.to change { project.runners.count }.by(-1)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user) }
end
end
@@ -469,7 +483,7 @@ describe API::Runners do
it "does not disable project's runner" do
expect do
delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
- end.to change{ project.runners.count }.by(0)
+ end.to change { project.runners.count }.by(0)
expect(response).to have_http_status(403)
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index c3ed5cd8ece..0b9a4b5c3db 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -19,6 +19,10 @@ describe API::Settings, 'Settings' do
expect(json_response['default_project_visibility']).to be_a String
expect(json_response['default_snippet_visibility']).to be_a String
expect(json_response['default_group_visibility']).to be_a String
+ expect(json_response['rsa_key_restriction']).to eq(0)
+ expect(json_response['dsa_key_restriction']).to eq(0)
+ expect(json_response['ecdsa_key_restriction']).to eq(0)
+ expect(json_response['ed25519_key_restriction']).to eq(0)
end
end
@@ -43,7 +47,13 @@ describe API::Settings, 'Settings' do
default_artifacts_expire_in: '2 days',
help_page_text: 'custom help text',
help_page_hide_commercial_content: true,
- help_page_support_url: 'http://example.com/help'
+ help_page_support_url: 'http://example.com/help',
+ project_export_enabled: false,
+ rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE,
+ dsa_key_restriction: 2048,
+ ecdsa_key_restriction: 384,
+ ed25519_key_restriction: 256
+
expect(response).to have_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
expect(json_response['password_authentication_enabled']).to be_falsey
@@ -58,6 +68,11 @@ describe API::Settings, 'Settings' do
expect(json_response['help_page_text']).to eq('custom help text')
expect(json_response['help_page_hide_commercial_content']).to be_truthy
expect(json_response['help_page_support_url']).to eq('http://example.com/help')
+ expect(json_response['project_export_enabled']).to be_falsey
+ expect(json_response['rsa_key_restriction']).to eq(ApplicationSetting::FORBIDDEN_KEY_VALUE)
+ expect(json_response['dsa_key_restriction']).to eq(2048)
+ expect(json_response['ecdsa_key_restriction']).to eq(384)
+ expect(json_response['ed25519_key_restriction']).to eq(256)
end
end
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 373fab4d98a..d3905f698bd 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -52,10 +52,10 @@ describe API::Snippets do
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
public_snippet_other.id)
- expect(json_response.map{ |snippet| snippet['web_url']} ).to include(
+ expect(json_response.map { |snippet| snippet['web_url']} ).to include(
"http://localhost/snippets/#{public_snippet.id}",
"http://localhost/snippets/#{public_snippet_other.id}")
- expect(json_response.map{ |snippet| snippet['raw_url']} ).to include(
+ expect(json_response.map { |snippet| snippet['raw_url']} ).to include(
"http://localhost/snippets/#{public_snippet.id}/raw",
"http://localhost/snippets/#{public_snippet_other.id}/raw")
end
@@ -137,7 +137,7 @@ describe API::Snippets do
end
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
context 'when the snippet is private' do
@@ -209,7 +209,7 @@ describe API::Snippets do
end
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
context 'when the snippet is private' do
@@ -270,6 +270,10 @@ describe API::Snippets do
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/snippets/#{public_snippet.id}", user) }
+ end
end
describe "GET /snippets/:id/user_agent_detail" do
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index f65b475fe44..216d278ad21 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -102,5 +102,9 @@ describe API::SystemHooks do
expect(response).to have_http_status(404)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/hooks/#{hook.id}", admin) }
+ end
end
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 9884c1ec206..0bf7863bdc8 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -278,12 +278,16 @@ describe API::Tags do
expect(response).to have_gitlab_http_status(204)
end
+ it_behaves_like '412 response' do
+ let(:request) { api(route, current_user) }
+ end
+
context 'when tag does not exist' do
let(:tag_name) { 'unknown' }
it_behaves_like '404 response' do
let(:request) { delete api(route, current_user) }
- let(:message) { 'No such tag' }
+ let(:message) { '404 Tag Not Found' }
end
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index d5c53b703dd..922b99a6cba 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -8,8 +8,8 @@ describe API::Triggers do
let!(:project) { create(:project, :repository, creator: user) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
- let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) }
- let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) }
+ let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token, owner: user) }
+ let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2, owner: user2) }
let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') }
describe 'POST /projects/:project_id/trigger/pipeline' do
@@ -22,7 +22,6 @@ describe API::Triggers do
before do
stub_ci_pipeline_to_return_yaml_file
- trigger.update(owner: user)
end
context 'Handles errors' do
@@ -85,6 +84,22 @@ describe API::Triggers do
expect(pipeline.variables.map { |v| { v.key => v.value } }.last).to eq(variables)
end
end
+
+ context 'when legacy trigger' do
+ before do
+ trigger.update(owner: nil)
+ end
+
+ it 'creates pipeline' do
+ post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'master')
+
+ expect(response).to have_http_status(201)
+ expect(json_response).to include('id' => pipeline.id)
+ pipeline.builds.reload
+ expect(pipeline.builds.pending.size).to eq(2)
+ expect(pipeline.builds.size).to eq(5)
+ end
+ end
end
context 'when triggering a pipeline from a trigger token' do
@@ -185,7 +200,7 @@ describe API::Triggers do
expect do
post api("/projects/#{project.id}/triggers", user),
description: 'trigger'
- end.to change{project.triggers.count}.by(1)
+ end.to change {project.triggers.count}.by(1)
expect(response).to have_http_status(201)
expect(json_response).to include('description' => 'trigger')
@@ -254,8 +269,6 @@ describe API::Triggers do
describe 'POST /projects/:id/triggers/:trigger_id/take_ownership' do
context 'authenticated user with valid permissions' do
it 'updates owner' do
- expect(trigger.owner).to be_nil
-
post api("/projects/#{project.id}/triggers/#{trigger.id}/take_ownership", user)
expect(response).to have_http_status(200)
@@ -288,7 +301,7 @@ describe API::Triggers do
delete api("/projects/#{project.id}/triggers/#{trigger.id}", user)
expect(response).to have_http_status(204)
- end.to change{project.triggers.count}.by(-1)
+ end.to change {project.triggers.count}.by(-1)
end
it 'responds with 404 Not Found if requesting non-existing trigger' do
@@ -296,6 +309,10 @@ describe API::Triggers do
expect(response).to have_http_status(404)
end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/triggers/#{trigger.id}", user) }
+ end
end
context 'authenticated user with invalid permissions' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 2dc7be22f8f..37cb95a16e3 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -4,6 +4,7 @@ describe API::Users do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
+ let(:gpg_key) { create(:gpg_key, user: user) }
let(:email) { create(:email, user: user) }
let(:omniauth_user) { create(:omniauth_user) }
let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') }
@@ -217,9 +218,19 @@ describe API::Users do
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
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(json_response['is_admin']).to be(false)
+ end
+ end
+
context 'for an anonymous user' do
it "returns a user by id" do
get api("/users/#{user.id}")
@@ -723,6 +734,10 @@ describe API::Users do
end.to change { user.keys.count }.by(-1)
end
+ it_behaves_like '412 response' do
+ let(:request) { api("/users/#{user.id}/keys/#{key.id}", admin) }
+ end
+
it 'returns 404 error if user not found' do
user.keys << key
user.save
@@ -739,6 +754,164 @@ describe API::Users do
end
end
+ describe 'POST /users/:id/keys' do
+ before do
+ admin
+ end
+
+ it 'does not create invalid GPG key' do
+ post api("/users/#{user.id}/gpg_keys", admin)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('key is missing')
+ end
+
+ it 'creates GPG key' do
+ key_attrs = attributes_for :gpg_key
+ expect do
+ post api("/users/#{user.id}/gpg_keys", admin), key_attrs
+
+ expect(response).to have_http_status(201)
+ end.to change { user.gpg_keys.count }.by(1)
+ end
+
+ it 'returns 400 for invalid ID' do
+ post api('/users/999999/gpg_keys', admin)
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ describe 'GET /user/:id/gpg_keys' do
+ before do
+ admin
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api("/users/#{user.id}/gpg_keys")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns 404 for non-existing user' do
+ get api('/users/999999/gpg_keys', admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns 404 error if key not foud' do
+ delete api("/users/#{user.id}/gpg_keys/42", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it 'returns array of GPG keys' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ get api("/users/#{user.id}/gpg_keys", admin)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['key']).to eq(gpg_key.key)
+ end
+ end
+ end
+
+ describe 'DELETE /user/:id/gpg_keys/:key_id' do
+ before do
+ admin
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ delete api("/users/#{user.id}/keys/42")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'deletes existing key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ expect do
+ delete api("/users/#{user.id}/gpg_keys/#{gpg_key.id}", admin)
+
+ expect(response).to have_http_status(204)
+ end.to change { user.gpg_keys.count }.by(-1)
+ end
+
+ it 'returns 404 error if user not found' do
+ user.keys << key
+ user.save
+
+ delete api("/users/999999/gpg_keys/#{gpg_key.id}", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns 404 error if key not foud' do
+ delete api("/users/#{user.id}/gpg_keys/42", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+ end
+ end
+
+ describe 'POST /user/:id/gpg_keys/:key_id/revoke' do
+ before do
+ admin
+ end
+
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ post api("/users/#{user.id}/gpg_keys/42/revoke")
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'revokes existing key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ expect do
+ post api("/users/#{user.id}/gpg_keys/#{gpg_key.id}/revoke", admin)
+
+ expect(response).to have_http_status(:accepted)
+ end.to change { user.gpg_keys.count }.by(-1)
+ end
+
+ it 'returns 404 error if user not found' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ post api("/users/999999/gpg_keys/#{gpg_key.id}/revoke", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 User Not Found')
+ end
+
+ it 'returns 404 error if key not foud' do
+ post api("/users/#{user.id}/gpg_keys/42/revoke", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+ end
+ end
+
describe "POST /users/:id/emails" do
before do
admin
@@ -828,6 +1001,10 @@ describe API::Users do
end.to change { user.emails.count }.by(-1)
end
+ it_behaves_like '412 response' do
+ let(:request) { api("/users/#{user.id}/emails/#{email.id}", admin) }
+ end
+
it 'returns 404 error if user not found' do
user.emails << email
user.save
@@ -866,6 +1043,10 @@ describe API::Users do
expect { Namespace.find(namespace.id) }.to raise_error ActiveRecord::RecordNotFound
end
+ it_behaves_like '412 response' do
+ let(:request) { api("/users/#{user.id}", admin) }
+ end
+
it "does not delete for unauthenticated user" do
Sidekiq::Testing.inline! { delete api("/users/#{user.id}") }
expect(response).to have_http_status(401)
@@ -1106,6 +1287,10 @@ describe API::Users do
end.to change { user.keys.count}.by(-1)
end
+ it_behaves_like '412 response' do
+ let(:request) { api("/user/keys/#{key.id}", user) }
+ end
+
it "returns 404 if key ID not found" do
delete api("/user/keys/42", user)
@@ -1127,6 +1312,173 @@ describe API::Users do
end
end
+ describe 'GET /user/gpg_keys' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/user/gpg_keys')
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns array of GPG keys' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ get api('/user/gpg_keys', user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first['key']).to eq(gpg_key.key)
+ end
+
+ context 'scopes' do
+ let(:path) { '/user/gpg_keys' }
+ let(:api_call) { method(:api) }
+
+ include_examples 'allows the "read_user" scope'
+ end
+ end
+ end
+
+ describe 'GET /user/gpg_keys/:key_id' do
+ it 'returns a single key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ get api("/user/gpg_keys/#{gpg_key.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['key']).to eq(gpg_key.key)
+ end
+
+ it 'returns 404 Not Found within invalid ID' do
+ get api('/user/gpg_keys/42', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it "returns 404 error if admin accesses user's GPG key" do
+ user.gpg_keys << gpg_key
+ user.save
+
+ get api("/user/gpg_keys/#{gpg_key.id}", admin)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it 'returns 404 for invalid ID' do
+ get api('/users/gpg_keys/ASDF', admin)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context 'scopes' do
+ let(:path) { "/user/gpg_keys/#{gpg_key.id}" }
+ let(:api_call) { method(:api) }
+
+ include_examples 'allows the "read_user" scope'
+ end
+ end
+
+ describe 'POST /user/gpg_keys' do
+ it 'creates a GPG key' do
+ key_attrs = attributes_for :gpg_key
+ expect do
+ post api('/user/gpg_keys', user), key_attrs
+
+ expect(response).to have_http_status(201)
+ end.to change { user.gpg_keys.count }.by(1)
+ end
+
+ it 'returns a 401 error if unauthorized' do
+ post api('/user/gpg_keys'), key: 'some key'
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'does not create GPG key without key' do
+ post api('/user/gpg_keys', user)
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('key is missing')
+ end
+ end
+
+ describe 'POST /user/gpg_keys/:key_id/revoke' do
+ it 'revokes existing GPG key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ expect do
+ post api("/user/gpg_keys/#{gpg_key.id}/revoke", user)
+
+ expect(response).to have_http_status(:accepted)
+ end.to change { user.gpg_keys.count}.by(-1)
+ end
+
+ it 'returns 404 if key ID not found' do
+ post api('/user/gpg_keys/42/revoke', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it 'returns 401 error if unauthorized' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ post api("/user/gpg_keys/#{gpg_key.id}/revoke")
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 404 for invalid ID' do
+ post api('/users/gpg_keys/ASDF/revoke', admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'DELETE /user/gpg_keys/:key_id' do
+ it 'deletes existing GPG key' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ expect do
+ delete api("/user/gpg_keys/#{gpg_key.id}", user)
+
+ expect(response).to have_http_status(204)
+ end.to change { user.gpg_keys.count}.by(-1)
+ end
+
+ it 'returns 404 if key ID not found' do
+ delete api('/user/gpg_keys/42', user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 GPG Key Not Found')
+ end
+
+ it 'returns 401 error if unauthorized' do
+ user.gpg_keys << gpg_key
+ user.save
+
+ delete api("/user/gpg_keys/#{gpg_key.id}")
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 404 for invalid ID' do
+ delete api('/users/gpg_keys/ASDF', admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
describe "GET /user/emails" do
context "when unauthenticated" do
it "returns authentication error" do
@@ -1229,6 +1581,10 @@ describe API::Users do
end.to change { user.emails.count}.by(-1)
end
+ it_behaves_like '412 response' do
+ let(:request) { api("/user/emails/#{email.id}", user) }
+ end
+
it "returns 404 if email ID not found" do
delete api("/user/emails/42", user)
@@ -1541,6 +1897,10 @@ describe API::Users do
expect(json_response['message']).to eq('403 Forbidden')
end
+ it_behaves_like '412 response' do
+ let(:request) { api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin) }
+ end
+
it 'revokes a impersonation token' do
delete api("/users/#{user.id}/impersonation_tokens/#{impersonation_token.id}", admin)
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
index 4a4a5dc5c7c..6d0ca33a6fa 100644
--- a/spec/requests/api/v3/commits_spec.rb
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -386,7 +386,7 @@ describe API::V3::Commits do
end
it "returns status for CI" do
- pipeline = project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha)
+ pipeline = project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha, protected: false)
pipeline.update(status: 'success')
get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
@@ -396,7 +396,7 @@ describe API::V3::Commits do
end
it "returns status for CI when pipeline is created" do
- project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha)
+ project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha, protected: false)
get v3_api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
@@ -474,7 +474,7 @@ describe API::V3::Commits do
expect(response).to have_http_status(201)
expect(json_response['title']).to eq(master_pickable_commit.title)
- expect(json_response['message']).to eq(master_pickable_commit.message)
+ expect(json_response['message']).to eq(master_pickable_commit.cherry_pick_message(user))
expect(json_response['author_name']).to eq(master_pickable_commit.author_name)
expect(json_response['committer_name']).to eq(user.name)
end
diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb
index 13a62423b1d..2affd0cfa51 100644
--- a/spec/requests/api/v3/deploy_keys_spec.rb
+++ b/spec/requests/api/v3/deploy_keys_spec.rb
@@ -87,7 +87,7 @@ describe API::V3::DeployKeys do
expect do
post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
- end.to change{ project.deploy_keys.count }.by(1)
+ end.to change { project.deploy_keys.count }.by(1)
end
it 'returns an existing ssh key when attempting to add a duplicate' do
@@ -122,7 +122,7 @@ describe API::V3::DeployKeys do
it 'should delete existing key' do
expect do
delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
- end.to change{ project.deploy_keys.count }.by(-1)
+ end.to change { project.deploy_keys.count }.by(-1)
end
it 'should return 404 Not Found with invalid ID' do
diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb
index 4ffa5d1784e..dc7f0eefd16 100644
--- a/spec/requests/api/v3/files_spec.rb
+++ b/spec/requests/api/v3/files_spec.rb
@@ -127,7 +127,7 @@ describe API::V3::Files do
it "returns a 400 if editor fails to create file" do
allow_any_instance_of(Repository).to receive(:create_file)
- .and_raise(Repository::CommitError, 'Cannot create file')
+ .and_raise(Gitlab::Git::CommitError, 'Cannot create file')
post v3_api("/projects/#{project.id}/repository/files", user), valid_params
@@ -228,7 +228,7 @@ describe API::V3::Files do
end
it "returns a 400 if fails to delete file" do
- allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Repository::CommitError, 'Cannot delete file')
+ allow_any_instance_of(Repository).to receive(:delete_file).and_raise(Gitlab::Git::CommitError, 'Cannot delete file')
delete v3_api("/projects/#{project.id}/repository/files", user), valid_params
diff --git a/spec/requests/api/v3/groups_spec.rb b/spec/requests/api/v3/groups_spec.rb
index 10756e494c3..778fcc73c30 100644
--- a/spec/requests/api/v3/groups_spec.rb
+++ b/spec/requests/api/v3/groups_spec.rb
@@ -504,7 +504,7 @@ describe API::V3::Groups do
let(:project) { create(:project) }
let(:project_path) { CGI.escape(project.full_path) }
- before(:each) do
+ before do
allow_any_instance_of(Projects::TransferService)
.to receive(:execute).and_return(true)
end
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 9eb538c4b09..9a0e6647ebf 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -884,7 +884,7 @@ describe API::V3::Issues, :mailer do
describe 'POST /projects/:id/issues with spam filtering' do
before do
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
- allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
+ allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
end
let(:params) do
@@ -1016,7 +1016,7 @@ describe API::V3::Issues, :mailer do
it "does not create a new project issue" do
allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
- allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
+ allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), params
diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb
index 8020ddab4c8..3f21ff40726 100644
--- a/spec/requests/api/v3/merge_request_diffs_spec.rb
+++ b/spec/requests/api/v3/merge_request_diffs_spec.rb
@@ -14,7 +14,7 @@ describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs' do
describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
it 'returns 200 for a valid merge request' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
- merge_request_diff = merge_request.merge_request_diffs.first
+ merge_request_diff = merge_request.merge_request_diffs.last
expect(response.status).to eq 200
expect(json_response.size).to eq(merge_request.merge_request_diffs.size)
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index ef7516fc28f..86f38dd4ec1 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -90,7 +90,7 @@ describe API::MergeRequests do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
@@ -99,7 +99,7 @@ describe API::MergeRequests do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -108,7 +108,7 @@ describe API::MergeRequests do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- response_dates = json_response.map{ |merge_request| merge_request['updated_at'] }
+ response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -117,7 +117,7 @@ describe API::MergeRequests do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
end
@@ -315,15 +315,17 @@ describe API::MergeRequests do
let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
- before :each do |each|
- fork_project.team << [user2, :reporter]
+ before do
+ fork_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),
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_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
expect(json_response['description']).to eq('Test description for Test merge_request')
end
@@ -334,7 +336,7 @@ describe API::MergeRequests do
expect(fork_project.forked_from_project).to eq(project)
post v3_api("/projects/#{fork_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_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq('Test merge_request')
end
@@ -348,25 +350,25 @@ describe API::MergeRequests do
author: user2,
target_project_id: project.id
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
it "returns 400 when source_branch is missing" do
post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
- expect(response).to have_http_status(400)
+ 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),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
- expect(response).to have_http_status(400)
+ 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),
target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
- expect(response).to have_http_status(400)
+ expect(response).to have_gitlab_http_status(400)
end
context 'when target_branch is specified' do
@@ -377,7 +379,7 @@ describe API::MergeRequests do
source_branch: 'markdown',
author: user,
target_project_id: fork_project.id
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
it 'returns 422 if targeting a different fork' do
@@ -387,14 +389,14 @@ describe API::MergeRequests do
source_branch: 'markdown',
author: user2,
target_project_id: unrelated_project.id
- expect(response).to have_http_status(422)
+ expect(response).to have_gitlab_http_status(422)
end
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
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
end
end
end
diff --git a/spec/requests/api/v3/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb
index 758fb482374..7e88489082a 100644
--- a/spec/requests/api/v3/project_snippets_spec.rb
+++ b/spec/requests/api/v3/project_snippets_spec.rb
@@ -30,7 +30,7 @@ describe API::ProjectSnippets do
expect(response).to have_http_status(200)
expect(json_response.size).to eq(3)
- expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
+ expect(json_response.map { |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
expect(json_response.last).to have_key('web_url')
end
@@ -80,7 +80,7 @@ describe API::ProjectSnippets do
end
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
context 'when the snippet is private' do
@@ -140,7 +140,7 @@ describe API::ProjectSnippets do
end
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
context 'when the snippet is private' do
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index fca5b5b5d82..cae2c3118da 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -687,6 +687,7 @@ describe API::V3::Projects do
expect(json_response['wiki_enabled']).to be_present
expect(json_response['builds_enabled']).to be_present
expect(json_response['snippets_enabled']).to be_present
+ expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions)
expect(json_response['container_registry_enabled']).to be_present
expect(json_response['created_at']).to be_present
expect(json_response['last_activity_at']).to be_present
@@ -1004,6 +1005,14 @@ describe API::V3::Projects do
expect(project_fork_target.forked?).to be_truthy
end
+ it 'refreshes the forks count cachce' do
+ expect(project_fork_source.forks_count).to be_zero
+
+ post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
+
+ expect(project_fork_source.forks_count).to eq(1)
+ end
+
it 'fails if forked_from project which does not exist' do
post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin)
expect(response).to have_http_status(404)
diff --git a/spec/requests/api/v3/runners_spec.rb b/spec/requests/api/v3/runners_spec.rb
index 78660afd840..a31eb3f1d43 100644
--- a/spec/requests/api/v3/runners_spec.rb
+++ b/spec/requests/api/v3/runners_spec.rb
@@ -38,7 +38,7 @@ describe API::V3::Runners do
delete v3_api("/runners/#{shared_runner.id}", admin)
expect(response).to have_http_status(200)
- end.to change{ Ci::Runner.shared.count }.by(-1)
+ end.to change { Ci::Runner.shared.count }.by(-1)
end
end
@@ -48,7 +48,7 @@ describe API::V3::Runners do
delete v3_api("/runners/#{unused_specific_runner.id}", admin)
expect(response).to have_http_status(200)
- end.to change{ Ci::Runner.specific.count }.by(-1)
+ end.to change { Ci::Runner.specific.count }.by(-1)
end
it 'deletes used runner' do
@@ -56,7 +56,7 @@ describe API::V3::Runners do
delete v3_api("/runners/#{specific_runner.id}", admin)
expect(response).to have_http_status(200)
- end.to change{ Ci::Runner.specific.count }.by(-1)
+ end.to change { Ci::Runner.specific.count }.by(-1)
end
end
@@ -91,7 +91,7 @@ describe API::V3::Runners do
delete v3_api("/runners/#{specific_runner.id}", user)
expect(response).to have_http_status(200)
- end.to change{ Ci::Runner.specific.count }.by(-1)
+ end.to change { Ci::Runner.specific.count }.by(-1)
end
end
end
@@ -113,7 +113,7 @@ describe API::V3::Runners do
delete v3_api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
expect(response).to have_http_status(200)
- end.to change{ project.runners.count }.by(-1)
+ end.to change { project.runners.count }.by(-1)
end
end
@@ -121,7 +121,7 @@ describe API::V3::Runners do
it "does not disable project's runner" do
expect do
delete v3_api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
- end.to change{ project.runners.count }.by(0)
+ end.to change { project.runners.count }.by(0)
expect(response).to have_http_status(403)
end
end
diff --git a/spec/requests/api/v3/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb
index 1bc2258ebd3..79860725634 100644
--- a/spec/requests/api/v3/snippets_spec.rb
+++ b/spec/requests/api/v3/snippets_spec.rb
@@ -45,10 +45,10 @@ describe API::V3::Snippets do
expect(json_response.map { |snippet| snippet['id']} ).to contain_exactly(
public_snippet.id,
public_snippet_other.id)
- expect(json_response.map{ |snippet| snippet['web_url']} ).to include(
+ expect(json_response.map { |snippet| snippet['web_url']} ).to include(
"http://localhost/snippets/#{public_snippet.id}",
"http://localhost/snippets/#{public_snippet_other.id}")
- expect(json_response.map{ |snippet| snippet['raw_url']} ).to include(
+ expect(json_response.map { |snippet| snippet['raw_url']} ).to include(
"http://localhost/snippets/#{public_snippet.id}/raw",
"http://localhost/snippets/#{public_snippet_other.id}/raw")
end
@@ -107,7 +107,7 @@ describe API::V3::Snippets do
end
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
context 'when the snippet is private' do
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
index 60212660fb6..7ccf387f2dc 100644
--- a/spec/requests/api/v3/triggers_spec.rb
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -7,7 +7,10 @@ describe API::V3::Triggers do
let!(:project) { create(:project, :repository, creator: user) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:developer) { create(:project_member, :developer, user: user2, project: project) }
- let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) }
+
+ let!(:trigger) do
+ create(:ci_trigger, project: project, token: trigger_token, owner: user)
+ end
describe 'POST /projects/:project_id/trigger' do
let!(:project2) { create(:project) }
@@ -34,7 +37,7 @@ describe API::V3::Triggers do
it 'returns unauthorized if token is for different project' do
post v3_api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
- expect(response).to have_http_status(401)
+ expect(response).to have_http_status(404)
end
end
@@ -77,7 +80,8 @@ describe API::V3::Triggers do
post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
expect(response).to have_http_status(201)
pipeline.builds.reload
- expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
+ expect(pipeline.variables.map { |v| { v.key => v.value } }.first).to eq(variables)
+ expect(json_response['variables']).to eq(variables)
end
end
end
@@ -171,7 +175,7 @@ describe API::V3::Triggers do
it 'creates trigger' do
expect do
post v3_api("/projects/#{project.id}/triggers", user)
- end.to change{project.triggers.count}.by(1)
+ end.to change {project.triggers.count}.by(1)
expect(response).to have_http_status(201)
expect(json_response).to be_a(Hash)
@@ -202,7 +206,7 @@ describe API::V3::Triggers do
delete v3_api("/projects/#{project.id}/triggers/#{trigger.token}", user)
expect(response).to have_http_status(200)
- end.to change{project.triggers.count}.by(-1)
+ end.to change {project.triggers.count}.by(-1)
end
it 'responds with 404 Not Found if requesting non-existing trigger' do
diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb
index bc0a4ab20a3..227b8d1b0c1 100644
--- a/spec/requests/api/v3/users_spec.rb
+++ b/spec/requests/api/v3/users_spec.rb
@@ -252,6 +252,31 @@ describe API::V3::Users do
end
context "as a user than can see the event's project" do
+ context 'when the list of events includes push events' do
+ let(:event) { create(:push_event, author: user, project: project) }
+ let!(:payload) { create(:push_event_payload, event: event) }
+ let(:payload_hash) { json_response[0]['push_data'] }
+
+ before do
+ get api("/users/#{user.id}/events?action=pushed", user)
+ end
+
+ it 'responds with HTTP 200 OK' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'includes the push payload as a Hash' do
+ expect(payload_hash).to be_an_instance_of(Hash)
+ end
+
+ it 'includes the push payload details' do
+ expect(payload_hash['commit_count']).to eq(payload.commit_count)
+ expect(payload_hash['action']).to eq(payload.action)
+ expect(payload_hash['ref_type']).to eq(payload.ref_type)
+ expect(payload_hash['commit_to']).to eq(payload.commit_to)
+ end
+ end
+
context 'joined event' do
it 'returns the "joined" event' do
get v3_api("/users/#{user.id}/events", user)
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 098a0d8ca7d..48592e12822 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -74,7 +74,7 @@ describe API::Variables do
it 'creates variable' do
expect do
post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true
- end.to change{project.variables.count}.by(1)
+ end.to change {project.variables.count}.by(1)
expect(response).to have_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
@@ -85,7 +85,7 @@ describe API::Variables do
it 'creates variable with optional attributes' do
expect do
post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
- end.to change{project.variables.count}.by(1)
+ end.to change {project.variables.count}.by(1)
expect(response).to have_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
@@ -96,7 +96,7 @@ describe API::Variables do
it 'does not allow to duplicate variable key' do
expect do
post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2'
- end.to change{project.variables.count}.by(0)
+ end.to change {project.variables.count}.by(0)
expect(response).to have_http_status(400)
end
@@ -166,7 +166,7 @@ describe API::Variables do
delete api("/projects/#{project.id}/variables/#{variable.key}", user)
expect(response).to have_http_status(204)
- end.to change{project.variables.count}.by(-1)
+ end.to change {project.variables.count}.by(-1)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
new file mode 100644
index 00000000000..9e889d1eecf
--- /dev/null
+++ b/spec/requests/api/wikis_spec.rb
@@ -0,0 +1,679 @@
+require 'spec_helper'
+
+# For every API endpoint we test 3 states of wikis:
+# - disabled
+# - enabled only for team members
+# - enabled for everyone who has access
+# Every state is tested for 3 user roles:
+# - guest
+# - developer
+# - master
+# because they are 3 edge cases of using wiki pages.
+
+describe API::Wikis do
+ let(:user) { create(:user) }
+ let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } }
+ let(:expected_keys_with_content) { %w(content format slug title) }
+ let(:expected_keys_without_content) { %w(format slug title) }
+
+ shared_examples_for 'returns list of wiki pages' do
+ context 'when wiki has pages' do
+ let!(:pages) do
+ [create(:wiki_page, wiki: project.wiki, attrs: { title: 'page1', content: 'content of page1' }),
+ create(:wiki_page, wiki: project.wiki, attrs: { title: 'page2', content: 'content of page2' })]
+ end
+
+ it 'returns the list of wiki pages without content' do
+ get api(url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+
+ json_response.each_with_index do |page, index|
+ expect(page.keys).to match_array(expected_keys_without_content)
+ expect(page['slug']).to eq(pages[index].slug)
+ expect(page['title']).to eq(pages[index].title)
+ end
+ end
+
+ it 'returns the list of wiki pages with content' do
+ get api(url, user), with_content: 1
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+
+ json_response.each_with_index do |page, index|
+ expect(page.keys).to match_array(expected_keys_with_content)
+ expect(page['content']).to eq(pages[index].content)
+ expect(page['slug']).to eq(pages[index].slug)
+ expect(page['title']).to eq(pages[index].title)
+ end
+ end
+ end
+
+ it 'return the empty list of wiki pages' do
+ get api(url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(0)
+ end
+ end
+
+ shared_examples_for 'returns wiki page' do
+ it 'returns the wiki page' do
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(4)
+ expect(json_response.keys).to match_array(expected_keys_with_content)
+ expect(json_response['content']).to eq(page.content)
+ expect(json_response['slug']).to eq(page.slug)
+ expect(json_response['title']).to eq(page.title)
+ end
+ end
+
+ shared_examples_for 'creates wiki page' do
+ it 'creates the wiki page' do
+ post(api(url, user), payload)
+
+ expect(response).to have_http_status(201)
+ expect(json_response.size).to eq(4)
+ expect(json_response.keys).to match_array(expected_keys_with_content)
+ expect(json_response['content']).to eq(payload[:content])
+ expect(json_response['slug']).to eq(payload[:title].tr(' ', '-'))
+ expect(json_response['title']).to eq(payload[:title])
+ expect(json_response['rdoc']).to eq(payload[:rdoc])
+ end
+
+ [:title, :content].each do |part|
+ it "responds with validation error on empty #{part}" do
+ payload.delete(part)
+
+ post(api(url, user), payload)
+
+ expect(response).to have_http_status(400)
+ expect(json_response.size).to eq(1)
+ expect(json_response['error']).to eq("#{part} is missing")
+ end
+ end
+ end
+
+ shared_examples_for 'updates wiki page' do
+ it 'updates the wiki page' do
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(4)
+ expect(json_response.keys).to match_array(expected_keys_with_content)
+ expect(json_response['content']).to eq(payload[:content])
+ expect(json_response['slug']).to eq(payload[:title].tr(' ', '-'))
+ expect(json_response['title']).to eq(payload[:title])
+ end
+ end
+
+ shared_examples_for '403 Forbidden' do
+ it 'returns 403 Forbidden' do
+ expect(response).to have_http_status(403)
+ expect(json_response.size).to eq(1)
+ expect(json_response['message']).to eq('403 Forbidden')
+ end
+ end
+
+ shared_examples_for '404 Wiki Page Not Found' do
+ it 'returns 404 Wiki Page Not Found' do
+ expect(response).to have_http_status(404)
+ expect(json_response.size).to eq(1)
+ expect(json_response['message']).to eq('404 Wiki Page Not Found')
+ end
+ end
+
+ shared_examples_for '404 Project Not Found' do
+ it 'returns 404 Project Not Found' do
+ expect(response).to have_http_status(404)
+ expect(json_response.size).to eq(1)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ shared_examples_for '204 No Content' do
+ it 'returns 204 No Content' do
+ expect(response).to have_http_status(204)
+ end
+ end
+
+ describe 'GET /projects/:id/wikis' do
+ let(:url) { "/projects/#{project.id}/wikis" }
+
+ context 'when wiki is disabled' do
+ let(:project) { create(:project, :wiki_disabled) }
+
+ context 'when user is guest' do
+ before do
+ get api(url)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+
+ get api(url, user)
+ end
+
+ include_examples '403 Forbidden'
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+
+ get api(url, user)
+ end
+
+ include_examples '403 Forbidden'
+ end
+ end
+
+ context 'when wiki is available only for team members' do
+ let(:project) { create(:project, :wiki_private) }
+
+ context 'when user is guest' do
+ before do
+ get api(url)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ include_examples 'returns list of wiki pages'
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+ end
+
+ include_examples 'returns list of wiki pages'
+ end
+ end
+
+ context 'when wiki is available for everyone with access' do
+ let(:project) { create(:project) }
+
+ context 'when user is guest' do
+ before do
+ get api(url)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ include_examples 'returns list of wiki pages'
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+ end
+
+ include_examples 'returns list of wiki pages'
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/wikis/:slug' do
+ let(:page) { create(:wiki_page, wiki: project.wiki) }
+ let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
+
+ context 'when wiki is disabled' do
+ let(:project) { create(:project, :wiki_disabled) }
+
+ context 'when user is guest' do
+ before do
+ get api(url)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+
+ get api(url, user)
+ end
+
+ include_examples '403 Forbidden'
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+
+ get api(url, user)
+ end
+
+ include_examples '403 Forbidden'
+ end
+ end
+
+ context 'when wiki is available only for team members' do
+ let(:project) { create(:project, :wiki_private) }
+
+ context 'when user is guest' do
+ before do
+ get api(url)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+ get api(url, user)
+ end
+
+ include_examples 'returns wiki page'
+
+ context 'when page is not existing' do
+ let(:url) { "/projects/#{project.id}/wikis/unknown" }
+
+ include_examples '404 Wiki Page Not Found'
+ end
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+
+ get api(url, user)
+ end
+
+ include_examples 'returns wiki page'
+
+ context 'when page is not existing' do
+ let(:url) { "/projects/#{project.id}/wikis/unknown" }
+
+ include_examples '404 Wiki Page Not Found'
+ end
+ end
+ end
+
+ context 'when wiki is available for everyone with access' do
+ let(:project) { create(:project) }
+
+ context 'when user is guest' do
+ before do
+ get api(url)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+
+ get api(url, user)
+ end
+
+ include_examples 'returns wiki page'
+
+ context 'when page is not existing' do
+ let(:url) { "/projects/#{project.id}/wikis/unknown" }
+
+ include_examples '404 Wiki Page Not Found'
+ end
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+
+ get api(url, user)
+ end
+
+ include_examples 'returns wiki page'
+
+ context 'when page is not existing' do
+ let(:url) { "/projects/#{project.id}/wikis/unknown" }
+
+ include_examples '404 Wiki Page Not Found'
+ end
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/wikis' do
+ let(:payload) { { title: 'title', content: 'content' } }
+ let(:url) { "/projects/#{project.id}/wikis" }
+
+ context 'when wiki is disabled' do
+ let(:project) { create(:project, :wiki_disabled) }
+
+ context 'when user is guest' do
+ before do
+ post(api(url), payload)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+ post(api(url, user), payload)
+ end
+
+ include_examples '403 Forbidden'
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+ post(api(url, user), payload)
+ end
+
+ include_examples '403 Forbidden'
+ end
+ end
+
+ context 'when wiki is available only for team members' do
+ let(:project) { create(:project, :wiki_private) }
+
+ context 'when user is guest' do
+ before do
+ post(api(url), payload)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ include_examples 'creates wiki page'
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+ end
+
+ include_examples 'creates wiki page'
+ end
+ end
+
+ context 'when wiki is available for everyone with access' do
+ let(:project) { create(:project) }
+
+ context 'when user is guest' do
+ before do
+ post(api(url), payload)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ include_examples 'creates wiki page'
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+ end
+
+ include_examples 'creates wiki page'
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/wikis/:slug' do
+ let(:page) { create(:wiki_page, wiki: project.wiki) }
+ let(:payload) { { title: 'new title', content: 'new content' } }
+ let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
+
+ context 'when wiki is disabled' do
+ let(:project) { create(:project, :wiki_disabled) }
+
+ context 'when user is guest' do
+ before do
+ put(api(url), payload)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+
+ put(api(url, user), payload)
+ end
+
+ include_examples '403 Forbidden'
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+
+ put(api(url, user), payload)
+ end
+
+ include_examples '403 Forbidden'
+ end
+ end
+
+ context 'when wiki is available only for team members' do
+ let(:project) { create(:project, :wiki_private) }
+
+ context 'when user is guest' do
+ before do
+ put(api(url), payload)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+
+ put(api(url, user), payload)
+ end
+
+ include_examples 'updates wiki page'
+
+ context 'when page is not existing' do
+ let(:url) { "/projects/#{project.id}/wikis/unknown" }
+
+ include_examples '404 Wiki Page Not Found'
+ end
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+
+ put(api(url, user), payload)
+ end
+
+ include_examples 'updates wiki page'
+
+ context 'when page is not existing' do
+ let(:url) { "/projects/#{project.id}/wikis/unknown" }
+
+ include_examples '404 Wiki Page Not Found'
+ end
+ end
+ end
+
+ context 'when wiki is available for everyone with access' do
+ let(:project) { create(:project) }
+
+ context 'when user is guest' do
+ before do
+ put(api(url), payload)
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+
+ put(api(url, user), payload)
+ end
+
+ include_examples 'updates wiki page'
+
+ context 'when page is not existing' do
+ let(:url) { "/projects/#{project.id}/wikis/unknown" }
+
+ include_examples '404 Wiki Page Not Found'
+ end
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+
+ put(api(url, user), payload)
+ end
+
+ include_examples 'updates wiki page'
+
+ context 'when page is not existing' do
+ let(:url) { "/projects/#{project.id}/wikis/unknown" }
+
+ include_examples '404 Wiki Page Not Found'
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/wikis/:slug' do
+ let(:page) { create(:wiki_page, wiki: project.wiki) }
+ let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
+
+ context 'when wiki is disabled' do
+ let(:project) { create(:project, :wiki_disabled) }
+
+ context 'when user is guest' do
+ before do
+ delete(api(url))
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+
+ delete(api(url, user))
+ end
+
+ include_examples '403 Forbidden'
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+
+ delete(api(url, user))
+ end
+
+ include_examples '403 Forbidden'
+ end
+ end
+
+ context 'when wiki is available only for team members' do
+ let(:project) { create(:project, :wiki_private) }
+
+ context 'when user is guest' do
+ before do
+ delete(api(url))
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+
+ delete(api(url, user))
+ end
+
+ include_examples '403 Forbidden'
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+
+ delete(api(url, user))
+ end
+
+ include_examples '204 No Content'
+ end
+ end
+
+ context 'when wiki is available for everyone with access' do
+ let(:project) { create(:project) }
+
+ context 'when user is guest' do
+ before do
+ delete(api(url))
+ end
+
+ include_examples '404 Project Not Found'
+ end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+
+ delete(api(url, user))
+ end
+
+ include_examples '403 Forbidden'
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+
+ delete(api(url, user))
+ end
+
+ include_examples '204 No Content'
+
+ context 'when page is not existing' do
+ let(:url) { "/projects/#{project.id}/wikis/unknown" }
+
+ include_examples '404 Wiki Page Not Found'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
deleted file mode 100644
index 2ebbe93da3e..00000000000
--- a/spec/requests/ci/api/builds_spec.rb
+++ /dev/null
@@ -1,923 +0,0 @@
-require 'spec_helper'
-
-describe Ci::API::Builds do
- let(:runner) { FactoryGirl.create(:ci_runner, tag_list: %w(mysql ruby)) }
- let(:project) { FactoryGirl.create(:project, shared_runners_enabled: false) }
- let(:last_update) { nil }
-
- describe "Builds API for runners" do
- let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
-
- before do
- project.runners << runner
- end
-
- describe "POST /builds/register" do
- let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- let(:user_agent) { 'gitlab-ci-multi-runner 1.5.2 (1-5-stable; go1.6.3; linux/amd64)' }
- let!(:last_update) { }
- let!(:new_update) { }
-
- before do
- stub_container_registry_config(enabled: false)
- end
-
- shared_examples 'no builds available' do
- context 'when runner sends version in User-Agent' do
- context 'for stable version' do
- it 'gives 204 and set X-GitLab-Last-Update' do
- expect(response).to have_http_status(204)
- expect(response.header).to have_key('X-GitLab-Last-Update')
- end
- end
-
- context 'when last_update is up-to-date' do
- let(:last_update) { runner.ensure_runner_queue_value }
-
- it 'gives 204 and set the same X-GitLab-Last-Update' do
- expect(response).to have_http_status(204)
- expect(response.header['X-GitLab-Last-Update'])
- .to eq(last_update)
- end
- end
-
- context 'when last_update is outdated' do
- let(:last_update) { runner.ensure_runner_queue_value }
- let(:new_update) { runner.tick_runner_queue }
-
- it 'gives 204 and set a new X-GitLab-Last-Update' do
- expect(response).to have_http_status(204)
- expect(response.header['X-GitLab-Last-Update'])
- .to eq(new_update)
- end
- end
-
- context 'for beta version' do
- let(:user_agent) { 'gitlab-ci-multi-runner 1.6.0~beta.167.g2b2bacc (1-5-stable; go1.6.3; linux/amd64)' }
- it { expect(response).to have_http_status(204) }
- end
- end
-
- context "when runner doesn't send version in User-Agent" do
- let(:user_agent) { 'Go-http-client/1.1' }
- it { expect(response).to have_http_status(404) }
- end
-
- context "when runner doesn't have a User-Agent" do
- let(:user_agent) { nil }
- it { expect(response).to have_http_status(404) }
- end
- end
-
- context 'when an old image syntax is used' do
- before do
- build.update!(options: { image: 'codeclimate' })
- end
-
- it 'starts a build' do
- register_builds info: { platform: :darwin }
-
- expect(response).to have_http_status(201)
- expect(json_response["options"]).to eq({ "image" => "codeclimate" })
- end
- end
-
- context 'when a new image syntax is used' do
- before do
- build.update!(options: { image: { name: 'codeclimate' } })
- end
-
- it 'starts a build' do
- register_builds info: { platform: :darwin }
-
- expect(response).to have_http_status(201)
- expect(json_response["options"]).to eq({ "image" => "codeclimate" })
- end
- end
-
- context 'when an old service syntax is used' do
- before do
- build.update!(options: { services: ['mysql'] })
- end
-
- it 'starts a build' do
- register_builds info: { platform: :darwin }
-
- expect(response).to have_http_status(201)
- expect(json_response["options"]).to eq({ "services" => ["mysql"] })
- end
- end
-
- context 'when a new service syntax is used' do
- before do
- build.update!(options: { services: [name: 'mysql'] })
- end
-
- it 'starts a build' do
- register_builds info: { platform: :darwin }
-
- expect(response).to have_http_status(201)
- expect(json_response["options"]).to eq({ "services" => ["mysql"] })
- end
- end
-
- context 'when no image or service is defined' do
- before do
- build.update!(options: {})
- end
-
- it 'starts a build' do
- register_builds info: { platform: :darwin }
-
- expect(response).to have_http_status(201)
-
- expect(json_response["options"]).to be_empty
- end
- end
-
- context 'when there is a pending build' do
- it 'starts a build' do
- register_builds info: { platform: :darwin }
-
- expect(response).to have_http_status(201)
- expect(response.headers).not_to have_key('X-GitLab-Last-Update')
- expect(json_response['sha']).to eq(build.sha)
- expect(runner.reload.platform).to eq("darwin")
- expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
- expect(json_response["variables"]).to include(
- { "key" => "CI_JOB_NAME", "value" => "spinach", "public" => true },
- { "key" => "CI_JOB_STAGE", "value" => "test", "public" => true },
- { "key" => "DB_NAME", "value" => "postgres", "public" => true }
- )
- end
-
- it 'updates runner info' do
- expect { register_builds }.to change { runner.reload.contacted_at }
- end
-
- context 'when concurrently updating build' do
- before do
- expect_any_instance_of(Ci::Build).to receive(:run!)
- .and_raise(ActiveRecord::StaleObjectError.new(nil, nil))
- end
-
- it 'returns a conflict' do
- register_builds info: { platform: :darwin }
-
- expect(response).to have_http_status(409)
- expect(response.headers).not_to have_key('X-GitLab-Last-Update')
- end
- end
-
- context 'registry credentials' do
- let(:registry_credentials) do
- { 'type' => 'registry',
- 'url' => 'registry.example.com:5005',
- 'username' => 'gitlab-ci-token',
- 'password' => build.token }
- end
-
- context 'when registry is enabled' do
- before do
- stub_container_registry_config(enabled: true, host_port: 'registry.example.com:5005')
- end
-
- it 'sends registry credentials key' do
- register_builds info: { platform: :darwin }
-
- expect(json_response).to have_key('credentials')
- expect(json_response['credentials']).to include(registry_credentials)
- end
- end
-
- context 'when registry is disabled' do
- before do
- stub_container_registry_config(enabled: false, host_port: 'registry.example.com:5005')
- end
-
- it 'does not send registry credentials' do
- register_builds info: { platform: :darwin }
-
- expect(json_response).to have_key('credentials')
- expect(json_response['credentials']).not_to include(registry_credentials)
- end
- end
- end
-
- context 'when docker configuration options are used' do
- let!(:build) { create(:ci_build, :extended_options, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
-
- it 'starts a build' do
- register_builds info: { platform: :darwin }
-
- expect(response).to have_http_status(201)
- expect(json_response['options']['image']).to eq('ruby:2.1')
- expect(json_response['options']['services']).to eq(['postgres', 'docker:dind'])
- end
- end
- end
-
- context 'when builds are finished' do
- before do
- build.success
- register_builds
- end
-
- it_behaves_like 'no builds available'
- end
-
- context 'for other project with builds' do
- before do
- build.success
- create(:ci_build, :pending)
- register_builds
- end
-
- it_behaves_like 'no builds available'
- end
-
- context 'for shared runner' do
- let!(:runner) { create(:ci_runner, :shared, token: "SharedRunner") }
-
- before do
- register_builds(runner.token)
- end
-
- it_behaves_like 'no builds available'
- end
-
- context 'for triggered build' do
- before do
- trigger = create(:ci_trigger, project: project)
- create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [build], trigger: trigger)
- project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
- end
-
- it "returns variables for triggers" do
- register_builds info: { platform: :darwin }
-
- expect(response).to have_http_status(201)
- expect(json_response["variables"]).to include(
- { "key" => "CI_JOB_NAME", "value" => "spinach", "public" => true },
- { "key" => "CI_JOB_STAGE", "value" => "test", "public" => true },
- { "key" => "CI_PIPELINE_TRIGGERED", "value" => "true", "public" => true },
- { "key" => "DB_NAME", "value" => "postgres", "public" => true },
- { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
- { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false }
- )
- end
- end
-
- context 'with multiple builds' do
- before do
- build.success
- end
-
- let!(:test_build) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
-
- it "returns dependent builds" do
- register_builds info: { platform: :darwin }
-
- expect(response).to have_http_status(201)
- expect(json_response["id"]).to eq(test_build.id)
- expect(json_response["depends_on_builds"].count).to eq(1)
- expect(json_response["depends_on_builds"][0]).to include('id' => build.id, 'name' => 'spinach')
- end
- end
-
- %w(name version revision platform architecture).each do |param|
- context "updates runner #{param}" do
- let(:value) { "#{param}_value" }
-
- subject { runner.read_attribute(param.to_sym) }
-
- it do
- register_builds info: { param => value }
-
- expect(response).to have_http_status(201)
- runner.reload
- is_expected.to eq(value)
- end
- end
- end
-
- context 'when build has no tags' do
- before do
- build.update(tags: [])
- end
-
- context 'when runner is allowed to pick untagged builds' do
- before do
- runner.update_column(:run_untagged, true)
- end
-
- it 'picks build' do
- register_builds
-
- expect(response).to have_http_status 201
- end
- end
-
- context 'when runner is not allowed to pick untagged builds' do
- before do
- runner.update_column(:run_untagged, false)
- register_builds
- end
-
- it_behaves_like 'no builds available'
- end
- end
-
- context 'when runner is paused' do
- let(:runner) { create(:ci_runner, :inactive, token: 'InactiveRunner') }
-
- it 'responds with 404' do
- register_builds
-
- expect(response).to have_http_status 404
- end
-
- it 'does not update runner info' do
- expect { register_builds }
- .not_to change { runner.reload.contacted_at }
- end
- end
-
- def register_builds(token = runner.token, **params)
- new_params = params.merge(token: token, last_update: last_update)
-
- post ci_api("/builds/register"), new_params, { 'User-Agent' => user_agent }
- end
- end
-
- describe "PUT /builds/:id" do
- let(:build) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) }
-
- before do
- build.run!
- put ci_api("/builds/#{build.id}"), token: runner.token
- end
-
- it "updates a running build" do
- expect(response).to have_http_status(200)
- end
-
- it 'does not override trace information when no trace is given' do
- expect(build.reload.trace.raw).to eq 'BUILD TRACE'
- end
-
- context 'job has been erased' do
- let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
-
- it 'responds with forbidden' do
- expect(response.status).to eq 403
- end
- end
- end
-
- describe 'PATCH /builds/:id/trace.txt' do
- let(:build) do
- attributes = { runner_id: runner.id, pipeline: pipeline }
- create(:ci_build, :running, :trace, attributes)
- end
-
- let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
- let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
- let(:update_interval) { 10.seconds.to_i }
-
- def patch_the_trace(content = ' appended', request_headers = nil)
- unless request_headers
- build.trace.read do |stream|
- offset = stream.size
- limit = offset + content.length - 1
- request_headers = headers.merge({ 'Content-Range' => "#{offset}-#{limit}" })
- end
- end
-
- Timecop.travel(build.updated_at + update_interval) do
- patch ci_api("/builds/#{build.id}/trace.txt"), content, request_headers
- build.reload
- end
- end
-
- def initial_patch_the_trace
- patch_the_trace(' appended', headers_with_range)
- end
-
- def force_patch_the_trace
- 2.times { patch_the_trace('') }
- end
-
- before do
- initial_patch_the_trace
- end
-
- context 'when request is valid' do
- it 'gets correct response' do
- expect(response.status).to eq 202
- expect(build.reload.trace.raw).to eq 'BUILD TRACE appended'
- expect(response.header).to have_key 'Range'
- expect(response.header).to have_key 'Build-Status'
- end
-
- context 'when build has been updated recently' do
- it { expect{ patch_the_trace }.not_to change { build.updated_at }}
-
- it 'changes the build trace' do
- patch_the_trace
-
- expect(build.reload.trace.raw).to eq 'BUILD TRACE appended appended'
- end
-
- context 'when Runner makes a force-patch' do
- it { expect{ force_patch_the_trace }.not_to change { build.updated_at }}
-
- it "doesn't change the build.trace" do
- force_patch_the_trace
-
- expect(build.reload.trace.raw).to eq 'BUILD TRACE appended'
- end
- end
- end
-
- context 'when build was not updated recently' do
- let(:update_interval) { 15.minutes.to_i }
-
- it { expect { patch_the_trace }.to change { build.updated_at } }
-
- it 'changes the build.trace' do
- patch_the_trace
-
- expect(build.reload.trace.raw).to eq 'BUILD TRACE appended appended'
- end
-
- context 'when Runner makes a force-patch' do
- it { expect { force_patch_the_trace }.to change { build.updated_at } }
-
- it "doesn't change the build.trace" do
- force_patch_the_trace
-
- expect(build.reload.trace.raw).to eq 'BUILD TRACE appended'
- end
- end
- end
-
- context 'when project for the build has been deleted' do
- let(:build) do
- attributes = { runner_id: runner.id, pipeline: pipeline }
- create(:ci_build, :running, :trace, attributes) do |build|
- build.project.update(pending_delete: true)
- end
- end
-
- it 'responds with forbidden' do
- expect(response.status).to eq(403)
- end
- end
- end
-
- context 'when Runner makes a force-patch' do
- before do
- force_patch_the_trace
- end
-
- it 'gets correct response' do
- expect(response.status).to eq 202
- expect(build.reload.trace.raw).to eq 'BUILD TRACE appended'
- expect(response.header).to have_key 'Range'
- expect(response.header).to have_key 'Build-Status'
- end
- end
-
- context 'when content-range start is too big' do
- let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }
-
- it 'gets 416 error response with range headers' do
- expect(response.status).to eq 416
- expect(response.header).to have_key 'Range'
- expect(response.header['Range']).to eq '0-11'
- end
- end
-
- context 'when content-range start is too small' do
- let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) }
-
- it 'gets 416 error response with range headers' do
- expect(response.status).to eq 416
- expect(response.header).to have_key 'Range'
- expect(response.header['Range']).to eq '0-11'
- end
- end
-
- context 'when Content-Range header is missing' do
- let(:headers_with_range) { headers }
-
- it { expect(response.status).to eq 400 }
- end
-
- context 'when build has been errased' do
- let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
-
- it { expect(response.status).to eq 403 }
- end
- end
-
- context "Artifacts" do
- let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
- let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
- let(:build) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
- let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") }
- let(:post_url) { ci_api("/builds/#{build.id}/artifacts") }
- let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") }
- let(:get_url) { ci_api("/builds/#{build.id}/artifacts") }
- let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
- let(:headers) { { "GitLab-Workhorse" => "1.0", Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
- let(:token) { build.token }
- let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => token) }
-
- before do
- stub_artifacts_object_storage
- build.run!
- end
-
- describe "POST /builds/:id/artifacts/authorize" do
- context "authorizes posting artifact to running build" do
- it "using token as parameter" do
- post authorize_url, { token: build.token }, headers
-
- expect(response).to have_http_status(200)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- expect(json_response["TempPath"]).not_to be_nil
- end
-
- it "using token as header" do
- post authorize_url, {}, headers_with_token
-
- expect(response).to have_http_status(200)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- expect(json_response["TempPath"]).not_to be_nil
- end
-
- it "using runners token" do
- post authorize_url, { token: build.project.runners_token }, headers
-
- expect(response).to have_http_status(200)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- expect(json_response["TempPath"]).not_to be_nil
- end
-
- it "reject requests that did not go through gitlab-workhorse" do
- headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
-
- post authorize_url, { token: build.token }, headers
-
- expect(response).to have_http_status(500)
- end
- end
-
- context "fails to post too large artifact" do
- it "using token as parameter" do
- stub_application_setting(max_artifacts_size: 0)
-
- post authorize_url, { token: build.token, filesize: 100 }, headers
-
- expect(response).to have_http_status(413)
- end
-
- it "using token as header" do
- stub_application_setting(max_artifacts_size: 0)
-
- post authorize_url, { filesize: 100 }, headers_with_token
-
- expect(response).to have_http_status(413)
- end
- end
-
- context 'authorization token is invalid' do
- before do
- post authorize_url, { token: 'invalid', filesize: 100 }
- end
-
- it 'responds with forbidden' do
- expect(response).to have_http_status(403)
- end
- end
- end
-
- describe "POST /builds/:id/artifacts" do
- context "disable sanitizer" do
- before do
- # by configuring this path we allow to pass temp file from any path
- allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
- end
-
- describe 'build has been erased' do
- let(:build) { create(:ci_build, erased_at: Time.now) }
-
- before do
- upload_artifacts(file_upload, headers_with_token)
- end
-
- it 'responds with forbidden' do
- expect(response.status).to eq 403
- end
- end
-
- describe 'uploading artifacts for a running build' do
- shared_examples 'successful artifacts upload' do
- it 'updates successfully' do
- response_filename =
- json_response['artifacts_file']['filename']
-
- expect(response).to have_http_status(201)
- expect(response_filename).to eq(file_upload.original_filename)
- end
- end
-
- context 'uses regular file post' do
- before do
- upload_artifacts(file_upload, headers_with_token, false)
- end
-
- it_behaves_like 'successful artifacts upload'
- end
-
- context 'uses accelerated file post' do
- before do
- upload_artifacts(file_upload, headers_with_token, true)
- end
-
- it_behaves_like 'successful artifacts upload'
- end
-
- context 'updates artifact' do
- before do
- upload_artifacts(file_upload2, headers_with_token)
- upload_artifacts(file_upload, headers_with_token)
- end
-
- it_behaves_like 'successful artifacts upload'
- end
-
- context 'when using runners token' do
- let(:token) { build.project.runners_token }
-
- before do
- upload_artifacts(file_upload, headers_with_token)
- end
-
- it_behaves_like 'successful artifacts upload'
- end
- end
-
- context 'posts artifacts file and metadata file' do
- let!(:artifacts) { file_upload }
- let!(:metadata) { file_upload2 }
-
- let(:stored_artifacts_file) { build.reload.artifacts_file.file }
- let(:stored_metadata_file) { build.reload.artifacts_metadata.file }
- let(:stored_artifacts_size) { build.reload.artifacts_size }
-
- before do
- post(post_url, post_data, headers_with_token)
- end
-
- context 'posts data accelerated by workhorse is correct' do
- let(:post_data) do
- { 'file.path' => artifacts.path,
- 'file.name' => artifacts.original_filename,
- 'metadata.path' => metadata.path,
- 'metadata.name' => metadata.original_filename }
- end
-
- it 'stores artifacts and artifacts metadata' do
- expect(response).to have_http_status(201)
- expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
- expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
- expect(stored_artifacts_size).to eq(71759)
- end
- end
-
- context 'no artifacts file in post data' do
- let(:post_data) do
- { 'metadata' => metadata }
- end
-
- it 'is expected to respond with bad request' do
- expect(response).to have_http_status(400)
- end
-
- it 'does not store metadata' do
- expect(stored_metadata_file).to be_nil
- end
- end
- end
-
- context 'with an expire date' do
- let!(:artifacts) { file_upload }
- let(:default_artifacts_expire_in) {}
-
- let(:post_data) do
- { 'file.path' => artifacts.path,
- 'file.name' => artifacts.original_filename,
- 'expire_in' => expire_in }
- end
-
- before do
- stub_application_setting(
- default_artifacts_expire_in: default_artifacts_expire_in)
-
- post(post_url, post_data, headers_with_token)
- end
-
- context 'with an expire_in given' do
- let(:expire_in) { '7 days' }
-
- it 'updates when specified' do
- build.reload
- expect(response).to have_http_status(201)
- expect(json_response['artifacts_expire_at']).not_to be_empty
- expect(build.artifacts_expire_at)
- .to be_within(5.minutes).of(7.days.from_now)
- end
- end
-
- context 'with no expire_in given' do
- let(:expire_in) { nil }
-
- it 'ignores if not specified' do
- build.reload
- expect(response).to have_http_status(201)
- expect(json_response['artifacts_expire_at']).to be_nil
- expect(build.artifacts_expire_at).to be_nil
- end
-
- context 'with application default' do
- context 'default to 5 days' do
- let(:default_artifacts_expire_in) { '5 days' }
-
- it 'sets to application default' do
- build.reload
- expect(response).to have_http_status(201)
- expect(json_response['artifacts_expire_at'])
- .not_to be_empty
- expect(build.artifacts_expire_at)
- .to be_within(5.minutes).of(5.days.from_now)
- end
- end
-
- context 'default to 0' do
- let(:default_artifacts_expire_in) { '0' }
-
- it 'does not set expire_in' do
- build.reload
- expect(response).to have_http_status(201)
- expect(json_response['artifacts_expire_at']).to be_nil
- expect(build.artifacts_expire_at).to be_nil
- end
- end
- end
- end
- end
-
- context "artifacts file is too large" do
- it "fails to post too large artifact" do
- stub_application_setting(max_artifacts_size: 0)
- upload_artifacts(file_upload, headers_with_token)
- expect(response).to have_http_status(413)
- end
- end
-
- context "artifacts post request does not contain file" do
- it "fails to post artifacts without file" do
- post post_url, {}, headers_with_token
- expect(response).to have_http_status(400)
- end
- end
-
- context 'GitLab Workhorse is not configured' do
- it "fails to post artifacts without GitLab-Workhorse" do
- post post_url, { token: build.token }, {}
- expect(response).to have_http_status(403)
- end
- end
- end
-
- context "artifacts are being stored outside of tmp path" do
- before do
- # by configuring this path we allow to pass file from @tmpdir only
- # but all temporary files are stored in system tmp directory
- @tmpdir = Dir.mktmpdir
- allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
- end
-
- after do
- FileUtils.remove_entry @tmpdir
- end
-
- it "fails to post artifacts for outside of tmp path" do
- upload_artifacts(file_upload, headers_with_token)
- expect(response).to have_http_status(400)
- end
- end
-
- def upload_artifacts(file, headers = {}, accelerated = true)
- if accelerated
- post post_url, {
- 'file.path' => file.path,
- 'file.name' => file.original_filename
- }, headers
- else
- post post_url, { file: file }, headers
- end
- end
- end
-
- describe 'DELETE /builds/:id/artifacts' do
- let(:build) { create(:ci_build, :artifacts) }
-
- before do
- delete delete_url, token: build.token
- end
-
- shared_examples 'having removable artifacts' do
- it 'removes build artifacts' do
- build.reload
-
- expect(response).to have_http_status(200)
- expect(build.artifacts_file.exists?).to be_falsy
- expect(build.artifacts_metadata.exists?).to be_falsy
- expect(build.artifacts_size).to be_nil
- end
- end
-
- context 'when using build token' do
- before do
- delete delete_url, token: build.token
- end
-
- it_behaves_like 'having removable artifacts'
- end
-
- context 'when using runnners token' do
- before do
- delete delete_url, token: build.project.runners_token
- end
-
- it_behaves_like 'having removable artifacts'
- end
- end
-
- describe 'GET /builds/:id/artifacts' do
- before do
- get get_url, token: token
- end
-
- context 'build has artifacts' do
- shared_examples 'having downloadable artifacts' do
- context 'when stored locally' do
- let(:build) { create(:ci_build, :artifacts) }
- let(:download_headers) do
- { 'Content-Transfer-Encoding' => 'binary',
- 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
- end
-
- it 'download artifacts' do
- expect(response).to have_http_status(200)
- expect(response.headers).to include download_headers
- end
- end
-
- context 'when stored remotely' do
- let(:build) { create(:ci_build, :artifacts, :remote_store) }
-
- it 'redirect to artifacts file' do
- expect(response).to have_http_status(302)
- end
- end
- end
-
- context 'when using build token' do
- let(:token) { build.token }
-
- it_behaves_like 'having downloadable artifacts'
- end
-
- context 'when using runnners token' do
- let(:token) { build.project.runners_token }
-
- it_behaves_like 'having downloadable artifacts'
- end
- end
-
- context 'build does not has artifacts' do
- let(:token) { build.token }
-
- it 'responds with not found' do
- expect(response).to have_http_status(404)
- end
- end
- end
- end
- end
-end
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
deleted file mode 100644
index 75059dd20a0..00000000000
--- a/spec/requests/ci/api/runners_spec.rb
+++ /dev/null
@@ -1,127 +0,0 @@
-require 'spec_helper'
-
-describe Ci::API::Runners do
- include StubGitlabCalls
-
- let(:registration_token) { 'abcdefg123456' }
-
- before do
- stub_gitlab_calls
- stub_application_setting(runners_registration_token: registration_token)
- end
-
- describe "POST /runners/register" do
- context 'when runner token is provided' do
- before do
- post ci_api("/runners/register"), token: registration_token
- end
-
- it 'creates runner with default values' do
- expect(response).to have_http_status 201
- expect(Ci::Runner.first.run_untagged).to be true
- expect(Ci::Runner.first.token).not_to eq(registration_token)
- end
- end
-
- context 'when runner description is provided' do
- before do
- post ci_api("/runners/register"), token: registration_token,
- description: "server.hostname"
- end
-
- it 'creates runner' do
- expect(response).to have_http_status 201
- expect(Ci::Runner.first.description).to eq("server.hostname")
- end
- end
-
- context 'when runner tags are provided' do
- before do
- post ci_api("/runners/register"), token: registration_token,
- tag_list: "tag1, tag2"
- end
-
- it 'creates runner' do
- expect(response).to have_http_status 201
- expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
- end
- end
-
- context 'when option for running untagged jobs is provided' do
- context 'when tags are provided' do
- it 'creates runner' do
- post ci_api("/runners/register"), token: registration_token,
- run_untagged: false,
- tag_list: ['tag']
-
- expect(response).to have_http_status 201
- expect(Ci::Runner.first.run_untagged).to be false
- end
- end
-
- context 'when tags are not provided' do
- it 'does not create runner' do
- post ci_api("/runners/register"), token: registration_token,
- run_untagged: false
-
- expect(response).to have_http_status 404
- end
- end
- end
-
- context 'when project token is provided' do
- let(:project) { FactoryGirl.create(:project) }
-
- before do
- post ci_api("/runners/register"), token: project.runners_token
- end
-
- it 'creates runner' do
- expect(response).to have_http_status 201
- expect(project.runners.size).to eq(1)
- expect(Ci::Runner.first.token).not_to eq(registration_token)
- expect(Ci::Runner.first.token).not_to eq(project.runners_token)
- end
- end
-
- context 'when token is invalid' do
- it 'returns 403 error' do
- post ci_api("/runners/register"), token: 'invalid'
-
- expect(response).to have_http_status 403
- end
- end
-
- context 'when no token provided' do
- it 'returns 400 error' do
- post ci_api("/runners/register")
-
- expect(response).to have_http_status 400
- end
- end
-
- %w(name version revision platform architecture).each do |param|
- context "creates runner with #{param} saved" do
- let(:value) { "#{param}_value" }
-
- subject { Ci::Runner.first.read_attribute(param.to_sym) }
-
- it do
- post ci_api("/runners/register"), token: registration_token, info: { param => value }
- expect(response).to have_http_status 201
- is_expected.to eq(value)
- end
- end
- end
- end
-
- describe "DELETE /runners/delete" do
- it 'returns 200' do
- runner = FactoryGirl.create(:ci_runner)
- delete ci_api("/runners/delete"), token: runner.token
-
- expect(response).to have_http_status 200
- expect(Ci::Runner.count).to eq(0)
- end
- end
-end
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
deleted file mode 100644
index 7c77ebb69a2..00000000000
--- a/spec/requests/ci/api/triggers_spec.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-require 'spec_helper'
-
-describe Ci::API::Triggers do
- describe 'POST /projects/:project_id/refs/:ref/trigger' do
- let!(:trigger_token) { 'secure token' }
- let!(:project) { create(:project, :repository, ci_id: 10) }
- let!(:project2) { create(:project, ci_id: 11) }
-
- let!(:trigger) do
- create(:ci_trigger,
- project: project,
- token: trigger_token,
- owner: create(:user))
- end
-
- let(:options) do
- {
- token: trigger_token
- }
- end
-
- before do
- stub_ci_pipeline_to_return_yaml_file
-
- project.add_developer(trigger.owner)
- end
-
- context 'Handles errors' do
- it 'returns bad request if token is missing' do
- post ci_api("/projects/#{project.ci_id}/refs/master/trigger")
- expect(response).to have_http_status(400)
- end
-
- it 'returns not found if project is not found' do
- post ci_api('/projects/0/refs/master/trigger'), options
- expect(response).to have_http_status(404)
- end
-
- it 'returns unauthorized if token is for different project' do
- post ci_api("/projects/#{project2.ci_id}/refs/master/trigger"), options
- expect(response).to have_http_status(401)
- end
- end
-
- context 'Have a commit' do
- let(:pipeline) { project.pipelines.last }
-
- it 'creates builds' do
- post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options
- expect(response).to have_http_status(201)
- pipeline.builds.reload
- expect(pipeline.builds.pending.size).to eq(2)
- expect(pipeline.builds.size).to eq(5)
- end
-
- it 'returns bad request with no builds created if there\'s no commit for that ref' do
- post ci_api("/projects/#{project.ci_id}/refs/other-branch/trigger"), options
- expect(response).to have_http_status(400)
- expect(json_response['message']['base'])
- .to contain_exactly('Reference not found')
- end
-
- context 'Validates variables' do
- let(:variables) do
- { 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
- end
-
- it 'validates variables to be a hash' do
- post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value')
- expect(response).to have_http_status(400)
-
- expect(json_response['error']).to eq('variables is invalid')
- end
-
- it 'validates variables needs to be a map of key-valued strings' do
- post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) })
- expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
- end
-
- it 'creates trigger request with variables' do
- post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: variables)
- expect(response).to have_http_status(201)
- pipeline.builds.reload
- expect(pipeline.builds.first.trigger_request.variables).to eq(variables)
- end
- end
- end
- end
-end
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index e5d9d3df5a8..6667ce771bd 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -93,7 +93,7 @@ describe 'cycle analytics events' do
context 'with private project and builds' do
before do
- project.members.first.update(access_level: Gitlab::Access::GUEST)
+ project.members.last.update(access_level: Gitlab::Access::GUEST)
end
it 'does not list the test events' do
diff --git a/spec/rubocop/cop/migration/safer_boolean_column_spec.rb b/spec/rubocop/cop/migration/safer_boolean_column_spec.rb
new file mode 100644
index 00000000000..1006594499a
--- /dev/null
+++ b/spec/rubocop/cop/migration/safer_boolean_column_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/safer_boolean_column'
+
+describe RuboCop::Cop::Migration::SaferBooleanColumn do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ described_class::SMALL_TABLES.each do |table|
+ context "for the #{table} table" do
+ sources_and_offense = [
+ ["add_column :#{table}, :column, :boolean, default: true", 'should disallow nulls'],
+ ["add_column :#{table}, :column, :boolean, default: false", 'should disallow nulls'],
+ ["add_column :#{table}, :column, :boolean, default: nil", 'should have a default and should disallow nulls'],
+ ["add_column :#{table}, :column, :boolean, null: false", 'should have a default'],
+ ["add_column :#{table}, :column, :boolean, null: true", 'should have a default and should disallow nulls'],
+ ["add_column :#{table}, :column, :boolean", 'should have a default and should disallow nulls'],
+ ["add_column :#{table}, :column, :boolean, default: nil, null: false", 'should have a default'],
+ ["add_column :#{table}, :column, :boolean, default: nil, null: true", 'should have a default and should disallow nulls'],
+ ["add_column :#{table}, :column, :boolean, default: false, null: true", 'should disallow nulls']
+ ]
+
+ sources_and_offense.each do |source, offense|
+ context "given the source \"#{source}\"" do
+ it "registers the offense matching \"#{offense}\"" do
+ inspect_source(cop, source)
+
+ aggregate_failures do
+ expect(cop.offenses.first.message).to match(offense)
+ end
+ end
+ end
+ end
+
+ inoffensive_sources = [
+ "add_column :#{table}, :column, :boolean, default: true, null: false",
+ "add_column :#{table}, :column, :boolean, default: false, null: false"
+ ]
+
+ inoffensive_sources.each do |source|
+ context "given the source \"#{source}\"" do
+ it "registers no offense" do
+ inspect_source(cop, source)
+
+ aggregate_failures do
+ expect(cop.offenses).to be_empty
+ end
+ end
+ end
+ end
+ end
+ end
+
+ it 'registers no offense for tables not listed in SMALL_TABLES' do
+ inspect_source(cop, "add_column :large_table, :column, :boolean")
+
+ expect(cop.offenses).to be_empty
+ end
+
+ it 'registers no offense for non-boolean columns' do
+ table = described_class::SMALL_TABLES.sample
+ inspect_source(cop, "add_column :#{table}, :column, :string")
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+
+ context 'outside of migration' do
+ it 'registers no offense' do
+ table = described_class::SMALL_TABLES.sample
+ inspect_source(cop, "add_column :#{table}, :column, :boolean")
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+end
diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb
index 86e703a6448..1ff4908972a 100644
--- a/spec/serializers/analytics_build_entity_spec.rb
+++ b/spec/serializers/analytics_build_entity_spec.rb
@@ -13,12 +13,8 @@ describe AnalyticsBuildEntity do
subject { entity.as_json }
- before do
- Timecop.freeze
- end
-
- after do
- Timecop.return
+ around do |example|
+ Timecop.freeze { example.run }
end
it 'contains the URL' do
@@ -47,7 +43,7 @@ describe AnalyticsBuildEntity do
let(:finished_at) { nil }
it 'does not blow up' do
- expect{ subject[:date] }.not_to raise_error
+ expect { subject[:date] }.not_to raise_error
end
it 'shows the right message' do
@@ -63,7 +59,7 @@ describe AnalyticsBuildEntity do
let(:started_at) { nil }
it 'does not blow up' do
- expect{ subject[:date] }.not_to raise_error
+ expect { subject[:date] }.not_to raise_error
end
it 'shows the right message' do
@@ -79,7 +75,7 @@ describe AnalyticsBuildEntity do
let(:finished_at) { nil }
it 'does not blow up' do
- expect{ subject[:date] }.not_to raise_error
+ expect { subject[:date] }.not_to raise_error
end
it 'shows the right message' do
diff --git a/spec/serializers/note_entity_spec.rb b/spec/serializers/note_entity_spec.rb
new file mode 100644
index 00000000000..3459cc72063
--- /dev/null
+++ b/spec/serializers/note_entity_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe NoteEntity do
+ include Gitlab::Routing
+
+ let(:request) { double('request', current_user: user, noteable: note.noteable) }
+
+ let(:entity) { described_class.new(note, request: request) }
+ let(:note) { create(:note) }
+ let(:user) { create(:user) }
+ subject { entity.as_json }
+
+ context 'basic note' do
+ it 'exposes correct elements' do
+ expect(subject).to include(:type, :author, :human_access, :note, :note_html, :current_user,
+ :discussion_id, :emoji_awardable, :award_emoji, :toggle_award_path, :report_abuse_path, :path, :attachment)
+ end
+
+ it 'does not expose elements for specific notes cases' do
+ expect(subject).not_to include(:last_edited_by, :last_edited_at, :system_note_icon_name)
+ end
+
+ it 'exposes author correctly' do
+ expect(subject[:author]).to include(:id, :name, :username, :state, :avatar_url, :path)
+ end
+
+ it 'does not expose web_url for author' do
+ expect(subject[:author]).not_to include(:web_url)
+ end
+ end
+
+ context 'when note was edited' do
+ before do
+ note.update(updated_at: 1.minute.from_now, updated_by: user)
+ end
+
+ it 'exposes last_edited_at and last_edited_by elements' do
+ expect(subject).to include(:last_edited_at, :last_edited_by)
+ end
+ end
+
+ context 'when note is a system note' do
+ before do
+ note.update(system: true)
+ end
+
+ it 'exposes system_note_icon_name element' do
+ expect(subject).to include(:system_note_icon_name)
+ end
+ end
+end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 881f2b6bfd8..f8df461bc81 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -36,7 +36,7 @@ describe PipelineEntity do
it 'contains flags' do
expect(subject).to include :flags
expect(subject[:flags])
- .to include :latest, :stuck,
+ .to include :latest, :stuck, :auto_devops,
:yaml_errors, :retryable, :cancelable
end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 362d754bca3..2de8daba6b5 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -111,7 +111,7 @@ describe PipelineSerializer do
shared_examples 'no N+1 queries' do
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.count).to be_within(1).of(59)
+ expect(recorded.count).to be_within(1).of(57)
expect(recorded.cached_count).to eq(0)
end
end
diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb
index f2ddaa903da..1a56164dba4 100644
--- a/spec/services/boards/issues/create_service_spec.rb
+++ b/spec/services/boards/issues/create_service_spec.rb
@@ -8,7 +8,7 @@ describe Boards::Issues::CreateService do
let(:label) { create(:label, project: project, name: 'in-progress') }
let!(:list) { create(:list, board: board, label: label, position: 0) }
- subject(:service) { described_class.new(project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
+ subject(:service) { described_class.new(board.parent, project, user, board_id: board.id, list_id: list.id, title: 'New issue') }
before do
project.team << [user, :developer]
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 63dfe80d672..464ff9f94b3 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -98,7 +98,7 @@ describe Boards::Issues::MoveService do
issue.move_to_end && issue.save!
end
- params.merge!(move_after_iid: issue1.iid, move_before_iid: issue2.iid)
+ params.merge!(move_after_id: issue1.id, move_before_id: issue2.id)
described_class.new(project, user, params).execute(issue)
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 730df4e0336..4c2ff08039c 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::CreatePipelineService do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
let(:user) { create(:admin) }
let(:ref_name) { 'refs/heads/master' }
@@ -55,10 +55,15 @@ describe Ci::CreatePipelineService do
context 'when merge requests already exist for this source branch' do
it 'updates head pipeline of each merge request' do
- merge_request_1 = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project)
- merge_request_2 = create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project)
+ merge_request_1 = create(:merge_request, source_branch: 'master',
+ target_branch: "branch_1",
+ source_project: project)
- head_pipeline = pipeline
+ merge_request_2 = create(:merge_request, source_branch: 'master',
+ target_branch: "branch_2",
+ source_project: project)
+
+ head_pipeline = execute_service
expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline)
expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline)
@@ -66,9 +71,11 @@ describe Ci::CreatePipelineService do
context 'when there is no pipeline for source branch' do
it "does not update merge request head pipeline" do
- merge_request = create(:merge_request, source_branch: 'other_branch', target_branch: "branch_1", source_project: project)
+ merge_request = create(:merge_request, source_branch: 'feature',
+ target_branch: "branch_1",
+ source_project: project)
- head_pipeline = pipeline
+ head_pipeline = execute_service
expect(merge_request.reload.head_pipeline).not_to eq(head_pipeline)
end
@@ -76,13 +83,19 @@ describe Ci::CreatePipelineService do
context 'when merge request target project is different from source project' do
let!(:target_project) { create(:project, :repository) }
- let!(:forked_project_link) { create(:forked_project_link, forked_to_project: project, forked_from_project: target_project) }
+
+ 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", source_project: project, target_project: target_project)
+ merge_request = create(:merge_request, source_branch: 'master',
+ target_branch: "branch_1",
+ source_project: project,
+ target_project: target_project)
- head_pipeline = pipeline
+ head_pipeline = execute_service
expect(merge_request.reload.head_pipeline).to eq(head_pipeline)
end
@@ -90,15 +103,36 @@ describe Ci::CreatePipelineService do
context 'when the pipeline is not the latest for the branch' do
it 'does not update merge request head pipeline' do
- merge_request = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project)
+ merge_request = create(:merge_request, source_branch: 'master',
+ target_branch: "branch_1",
+ source_project: project)
- allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(false)
+ allow_any_instance_of(Ci::Pipeline)
+ .to receive(:latest?).and_return(false)
- pipeline
+ execute_service
expect(merge_request.reload.head_pipeline).to be_nil
end
end
+
+ context 'when pipeline has errors' do
+ before do
+ stub_ci_pipeline_yaml_file('some invalid syntax')
+ end
+
+ it 'updates merge request head pipeline reference' do
+ merge_request = create(:merge_request, source_branch: 'master',
+ target_branch: 'feature',
+ source_project: project)
+
+ head_pipeline = execute_service
+
+ expect(head_pipeline).to be_persisted
+ expect(head_pipeline.yaml_errors).to be_present
+ expect(merge_request.reload.head_pipeline).to eq head_pipeline
+ end
+ end
end
context 'auto-cancel enabled' do
@@ -357,12 +391,15 @@ describe Ci::CreatePipelineService do
end
context 'when user is master' do
+ let(:pipeline) { execute_service }
+
before do
project.add_master(user)
end
- it 'creates a pipeline' do
- expect(execute_service).to be_persisted
+ it 'creates a protected pipeline' do
+ expect(pipeline).to be_persisted
+ expect(pipeline).to be_protected
expect(Ci::Pipeline.count).to eq(1)
end
end
@@ -379,14 +416,12 @@ describe Ci::CreatePipelineService do
end
context 'when trigger belongs to a developer' do
- let(:user) {}
+ let(:user) { create(:user) }
+ let(:trigger) { create(:ci_trigger, owner: user) }
+ let(:trigger_request) { create(:ci_trigger_request, trigger: trigger) }
- let(:trigger_request) do
- create(:ci_trigger_request).tap do |request|
- user = create(:user)
- project.add_developer(user)
- request.trigger.update(owner: user)
- end
+ before do
+ project.add_developer(user)
end
it 'does not create a pipeline' do
@@ -397,17 +432,15 @@ describe Ci::CreatePipelineService do
end
context 'when trigger belongs to a master' do
- let(:user) {}
+ let(:user) { create(:user) }
+ let(:trigger) { create(:ci_trigger, owner: user) }
+ let(:trigger_request) { create(:ci_trigger_request, trigger: trigger) }
- let(:trigger_request) do
- create(:ci_trigger_request).tap do |request|
- user = create(:user)
- project.add_master(user)
- request.trigger.update(owner: user)
- end
+ before do
+ project.add_master(user)
end
- it 'does not create a pipeline' do
+ it 'creates a pipeline' do
expect(execute_service(trigger_request: trigger_request))
.to be_persisted
expect(Ci::Pipeline.count).to eq(1)
@@ -436,11 +469,13 @@ describe Ci::CreatePipelineService do
context 'when ref is not protected' do
context 'when trigger belongs to no one' do
let(:user) {}
- let(:trigger_request) { create(:ci_trigger_request) }
+ let(:trigger) { create(:ci_trigger, owner: nil) }
+ let(:trigger_request) { create(:ci_trigger_request, trigger: trigger) }
+ let(:pipeline) { execute_service(trigger_request: trigger_request) }
- it 'creates a pipeline' do
- expect(execute_service(trigger_request: trigger_request))
- .to be_persisted
+ it 'creates an unprotected pipeline' do
+ expect(pipeline).to be_persisted
+ expect(pipeline).not_to be_protected
expect(Ci::Pipeline.count).to eq(1)
end
end
@@ -454,7 +489,7 @@ describe Ci::CreatePipelineService do
subject do
described_class.new(project, user, ref: ref)
- .send(:allowed_to_create?, user)
+ .send(:allowed_to_create?)
end
context 'when user is a developer' do
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
deleted file mode 100644
index 8295813a1ca..00000000000
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-require 'spec_helper'
-
-describe Ci::CreateTriggerRequestService do
- let(:service) { described_class }
- let(:project) { create(:project, :repository) }
- let(:trigger) { create(:ci_trigger, project: project, owner: owner) }
- let(:owner) { create(:user) }
-
- before do
- stub_ci_pipeline_to_return_yaml_file
-
- project.add_developer(owner)
- end
-
- describe '#execute' do
- context 'valid params' do
- subject { service.execute(project, trigger, 'master') }
-
- context 'without owner' do
- it { expect(subject.trigger_request).to be_kind_of(Ci::TriggerRequest) }
- it { expect(subject.trigger_request.builds.first).to be_kind_of(Ci::Build) }
- it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) }
- it { expect(subject.pipeline).to be_trigger }
- end
-
- context 'with owner' do
- it { expect(subject.trigger_request).to be_kind_of(Ci::TriggerRequest) }
- it { expect(subject.trigger_request.builds.first).to be_kind_of(Ci::Build) }
- it { expect(subject.trigger_request.builds.first.user).to eq(owner) }
- it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) }
- it { expect(subject.pipeline).to be_trigger }
- it { expect(subject.pipeline.user).to eq(owner) }
- end
- end
-
- context 'no commit for ref' do
- subject { service.execute(project, trigger, 'other-branch') }
-
- it { expect(subject.pipeline).not_to be_persisted }
- end
-
- context 'no builds created' do
- subject { service.execute(project, trigger, 'master') }
-
- before do
- stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }')
- end
-
- it { expect(subject.pipeline).not_to be_persisted }
- end
- end
-end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 8eb0d2d10a4..5ac30111ec9 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -4,7 +4,7 @@ module Ci
describe RegisterJobService do
let!(:project) { FactoryGirl.create :project, shared_runners_enabled: false }
let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
- let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline }
+ let!(:pending_job) { FactoryGirl.create :ci_build, pipeline: pipeline }
let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) }
let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) }
@@ -15,32 +15,32 @@ module Ci
describe '#execute' do
context 'runner follow tag list' do
it "picks build with the same tag" do
- pending_build.tag_list = ["linux"]
- pending_build.save
+ pending_job.tag_list = ["linux"]
+ pending_job.save
specific_runner.tag_list = ["linux"]
- expect(execute(specific_runner)).to eq(pending_build)
+ expect(execute(specific_runner)).to eq(pending_job)
end
it "does not pick build with different tag" do
- pending_build.tag_list = ["linux"]
- pending_build.save
+ pending_job.tag_list = ["linux"]
+ pending_job.save
specific_runner.tag_list = ["win32"]
expect(execute(specific_runner)).to be_falsey
end
it "picks build without tag" do
- expect(execute(specific_runner)).to eq(pending_build)
+ expect(execute(specific_runner)).to eq(pending_job)
end
it "does not pick build with tag" do
- pending_build.tag_list = ["linux"]
- pending_build.save
+ pending_job.tag_list = ["linux"]
+ pending_job.save
expect(execute(specific_runner)).to be_falsey
end
it "pick build without tag" do
specific_runner.tag_list = ["win32"]
- expect(execute(specific_runner)).to eq(pending_build)
+ expect(execute(specific_runner)).to eq(pending_job)
end
end
@@ -76,7 +76,7 @@ module Ci
let!(:pipeline2) { create :ci_pipeline, project: project2 }
let!(:project3) { create :project, shared_runners_enabled: true }
let!(:pipeline3) { create :ci_pipeline, project: project3 }
- let!(:build1_project1) { pending_build }
+ let!(:build1_project1) { pending_job }
let!(:build2_project1) { FactoryGirl.create :ci_build, pipeline: pipeline }
let!(:build3_project1) { FactoryGirl.create :ci_build, pipeline: pipeline }
let!(:build1_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 }
@@ -172,7 +172,7 @@ module Ci
context 'when first build is stalled' do
before do
- pending_build.lock_version = 10
+ pending_job.lock_version = 10
end
subject { described_class.new(specific_runner).execute }
@@ -182,7 +182,7 @@ module Ci
before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
- .and_return([pending_build, other_build])
+ .and_return([pending_job, other_build])
end
it "receives second build from the queue" do
@@ -194,7 +194,7 @@ module Ci
context 'when single build is in queue' do
before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
- .and_return([pending_build])
+ .and_return([pending_job])
end
it "does not receive any valid result" do
@@ -215,6 +215,70 @@ module Ci
end
end
+ context 'when access_level of runner is not_protected' do
+ let!(:specific_runner) { create(:ci_runner, :specific) }
+
+ context 'when a job is protected' do
+ let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) }
+
+ it 'picks the job' do
+ expect(execute(specific_runner)).to eq(pending_job)
+ end
+ end
+
+ context 'when a job is unprotected' do
+ let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
+
+ it 'picks the job' do
+ expect(execute(specific_runner)).to eq(pending_job)
+ end
+ end
+
+ context 'when protected attribute of a job is nil' do
+ let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
+
+ before do
+ pending_job.update_attribute(:protected, nil)
+ end
+
+ it 'picks the job' do
+ expect(execute(specific_runner)).to eq(pending_job)
+ end
+ end
+ end
+
+ context 'when access_level of runner is ref_protected' do
+ let!(:specific_runner) { create(:ci_runner, :ref_protected, :specific) }
+
+ context 'when a job is protected' do
+ let!(:pending_job) { create(:ci_build, :protected, pipeline: pipeline) }
+
+ it 'picks the job' do
+ expect(execute(specific_runner)).to eq(pending_job)
+ end
+ end
+
+ context 'when a job is unprotected' do
+ let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
+
+ it 'does not pick the job' do
+ expect(execute(specific_runner)).to be_nil
+ end
+ end
+
+ context 'when protected attribute of a job is nil' do
+ let!(:pending_job) { create(:ci_build, pipeline: pipeline) }
+
+ before do
+ pending_job.update_attribute(:protected, nil)
+ end
+
+ it 'does not pick the job' do
+ expect(execute(specific_runner)).to be_nil
+ end
+ end
+ end
+
def execute(runner)
described_class.new(runner).execute.build
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 81599875f5b..7f29fac9eef 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -22,7 +22,7 @@ describe Ci::RetryBuildService do
%i[type lock_version target_url base_tags
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
+ user_id auto_canceled_by_id retried failure_reason
artifacts_file_store artifacts_metadata_store].freeze
shared_examples 'build duplication' do
@@ -49,10 +49,21 @@ describe Ci::RetryBuildService do
describe 'clone accessors' do
CLONE_ACCESSORS.each do |attribute|
it "clones #{attribute} build attribute" do
- expect(new_build.send(attribute)).to be_present
+ expect(new_build.send(attribute)).not_to be_nil
expect(new_build.send(attribute)).to eq build.send(attribute)
end
end
+
+ context 'when job has nullified protected' do
+ before do
+ build.update_attribute(:protected, nil)
+ end
+
+ it "clones protected build attribute" do
+ expect(new_build.protected).to be_nil
+ expect(new_build.protected).to eq build.protected
+ end
+ end
end
describe 'reject acessors' do
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 049b082277a..08267d6e6a0 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -20,6 +20,10 @@ describe CreateDeploymentService do
let(:service) { described_class.new(job) }
+ before do
+ allow_any_instance_of(Deployment).to receive(:create_ref)
+ end
+
describe '#execute' do
subject { service.execute }
diff --git a/spec/services/discussions/update_diff_position_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb
index c239494298b..82b156f5ebe 100644
--- a/spec/services/discussions/update_diff_position_service_spec.rb
+++ b/spec/services/discussions/update_diff_position_service_spec.rb
@@ -150,21 +150,7 @@ describe Discussions::UpdateDiffPositionService do
)
end
- context "when the diff line is the same" do
- let(:line) { 16 }
-
- it "updates the position" do
- subject.execute(discussion)
-
- expect(discussion.original_position).to eq(old_position)
- expect(discussion.position).not_to eq(old_position)
- expect(discussion.position.new_line).to eq(22)
- end
- end
-
- context "when the diff line has changed" do
- let(:line) { 9 }
-
+ shared_examples 'outdated diff note' do
it "doesn't update the position" do
subject.execute(discussion)
@@ -189,5 +175,51 @@ describe Discussions::UpdateDiffPositionService do
subject.execute(discussion)
end
end
+
+ context "when the diff line is the same" do
+ let(:line) { 16 }
+
+ it "updates the position" do
+ subject.execute(discussion)
+
+ expect(discussion.original_position).to eq(old_position)
+ expect(discussion.position).not_to eq(old_position)
+ expect(discussion.position.new_line).to eq(22)
+ end
+
+ context 'when the resolve_outdated_diff_discussions setting is set' do
+ before do
+ project.update!(resolve_outdated_diff_discussions: true)
+ end
+
+ it 'does not resolve the discussion' do
+ subject.execute(discussion)
+
+ expect(discussion).not_to be_resolved
+ expect(discussion).not_to be_resolved_by_push
+ end
+ end
+ end
+
+ context "when the diff line has changed" do
+ let(:line) { 9 }
+
+ include_examples 'outdated diff note'
+
+ context 'when the resolve_outdated_diff_discussions setting is set' do
+ before do
+ project.update!(resolve_outdated_diff_discussions: true)
+ end
+
+ it 'sets resolves the discussion and sets resolved_by_push' do
+ subject.execute(discussion)
+
+ expect(discussion).to be_resolved
+ expect(discussion).to be_resolved_by_push
+ end
+
+ include_examples 'outdated diff note'
+ end
+ end
end
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 42adb044190..02d7ddeb86b 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -117,12 +117,52 @@ describe EventCreateService do
let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:push_data) do
+ {
+ commits: [
+ {
+ id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ message: 'This is a commit'
+ }
+ ],
+ before: '0000000000000000000000000000000000000000',
+ after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ total_commits_count: 1,
+ ref: 'refs/heads/my-branch'
+ }
+ end
+
it 'creates a new event' do
- expect { service.push(project, user, {}) }.to change { Event.count }
+ expect { service.push(project, user, push_data) }.to change { Event.count }
+ end
+
+ it 'creates the push event payload' do
+ expect(PushEventPayloadService).to receive(:new)
+ .with(an_instance_of(PushEvent), push_data)
+ .and_call_original
+
+ service.push(project, user, push_data)
end
it 'updates user last activity' do
- expect { service.push(project, user, {}) }.to change { user_activity(user) }
+ expect { service.push(project, user, push_data) }
+ .to change { user_activity(user) }
+ end
+
+ it 'does not create any event data when an error is raised' do
+ payload_service = double(:service)
+
+ allow(payload_service).to receive(:execute)
+ .and_raise(RuntimeError)
+
+ allow(PushEventPayloadService).to receive(:new)
+ .and_return(payload_service)
+
+ expect { service.push(project, user, push_data) }
+ .to raise_error(RuntimeError)
+
+ expect(Event.count).to eq(0)
+ expect(PushEventPayload.count).to eq(0)
end
end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index cc950ae6bb3..2b4f8cd42ee 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -76,7 +76,7 @@ describe Files::UpdateService do
let(:branch_name) { "#{project.default_branch}-new" }
it 'fires hooks only once' do
- expect(GitHooksService).to receive(:new).once.and_call_original
+ expect(Gitlab::Git::HooksService).to receive(:new).once.and_call_original
subject.execute
end
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
deleted file mode 100644
index 3ce01a995b4..00000000000
--- a/spec/services/git_hooks_service_spec.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'spec_helper'
-
-describe GitHooksService do
- include RepoHelpers
-
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:service) { described_class.new }
-
- before do
- @blankrev = Gitlab::Git::BLANK_SHA
- @oldrev = sample_commit.parent_id
- @newrev = sample_commit.id
- @ref = 'refs/heads/feature'
- end
-
- describe '#execute' do
- context 'when receive hooks were successful' do
- it 'calls post-receive hook' do
- hook = double(trigger: [true, nil])
- expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
-
- service.execute(user, project, @blankrev, @newrev, @ref) { }
- end
- end
-
- context 'when pre-receive hook failed' do
- it 'does not call post-receive hook' do
- expect(service).to receive(:run_hook).with('pre-receive').and_return([false, ''])
- expect(service).not_to receive(:run_hook).with('post-receive')
-
- expect do
- service.execute(user, project, @blankrev, @newrev, @ref)
- end.to raise_error(GitHooksService::PreReceiveError)
- end
- end
-
- context 'when update hook failed' do
- it 'does not call post-receive hook' do
- expect(service).to receive(:run_hook).with('pre-receive').and_return([true, nil])
- expect(service).to receive(:run_hook).with('update').and_return([false, ''])
- expect(service).not_to receive(:run_hook).with('post-receive')
-
- expect do
- service.execute(user, project, @blankrev, @newrev, @ref)
- end.to raise_error(GitHooksService::PreReceiveError)
- end
- end
- end
-end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 82724ccd281..cc3d4e7da49 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -132,7 +132,7 @@ describe GitPushService, services: true do
end
it "creates a new pipeline" do
- expect{ subject }.to change{ Ci::Pipeline.count }
+ expect { subject }.to change { Ci::Pipeline.count }
expect(Ci::Pipeline.last).to be_push
end
end
@@ -141,10 +141,13 @@ describe GitPushService, services: true do
let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) }
let(:event) { Event.find_by_action(Event::PUSHED) }
- it { expect(event).not_to be_nil }
+ it { expect(event).to be_an_instance_of(PushEvent) }
it { expect(event.project).to eq(project) }
it { expect(event.action).to eq(Event::PUSHED) }
- it { expect(event.data).to eq(push_data) }
+ it { expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) }
+ it { expect(event.push_event_payload.commit_from).to eq(oldrev) }
+ it { expect(event.push_event_payload.commit_to).to eq(newrev) }
+ it { expect(event.push_event_payload.ref).to eq('master') }
context "Updates merge requests" do
it "when pushing a new branch for the first time" do
@@ -614,7 +617,7 @@ describe GitPushService, services: true do
context 'on the default branch' do
before do
- allow(service).to receive(:is_default_branch?).and_return(true)
+ allow(service).to receive(:default_branch?).and_return(true)
end
it 'flushes the caches of any special files that have been changed' do
@@ -635,7 +638,7 @@ describe GitPushService, services: true do
context 'on a non-default branch' do
before do
- allow(service).to receive(:is_default_branch?).and_return(false)
+ allow(service).to receive(:default_branch?).and_return(false)
end
it 'does not flush any conditional caches' do
@@ -685,10 +688,38 @@ describe GitPushService, services: true do
)
end
- it 'calls CreateGpgSignatureWorker.perform_async for each commit' do
- expect(CreateGpgSignatureWorker).to receive(:perform_async).with(sample_commit.id, project.id)
+ context 'when the commit has a signature' do
+ context 'when the signature is already cached' do
+ before do
+ create(:gpg_signature, commit_sha: sample_commit.id)
+ end
- execute_service(project, user, oldrev, newrev, ref)
+ it 'does not queue a CreateGpgSignatureWorker' do
+ expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id)
+
+ execute_service(project, user, oldrev, newrev, ref)
+ end
+ end
+
+ context 'when the signature is not yet cached' do
+ it 'queues a CreateGpgSignatureWorker' do
+ expect(CreateGpgSignatureWorker).to receive(:perform_async).with(sample_commit.id, project.id)
+
+ execute_service(project, user, oldrev, newrev, ref)
+ end
+ end
+ end
+
+ context 'when the commit does not have a signature' do
+ before do
+ allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).with(project.repository, [sample_commit.id]).and_return([])
+ end
+
+ it 'does not queue a CreateGpgSignatureWorker' do
+ expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id)
+
+ execute_service(project, user, oldrev, newrev, ref)
+ end
end
end
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
index f877c145390..05695aa8188 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -39,7 +39,7 @@ describe GitTagPushService do
end
it "creates a new pipeline" do
- expect{ subject }.to change{ Ci::Pipeline.count }
+ expect { subject }.to change { Ci::Pipeline.count }
expect(Ci::Pipeline.last).to be_push
end
end
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index b2175717a70..224e933bebc 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -22,7 +22,27 @@ describe Groups::CreateService, '#execute' do
end
end
- describe 'creating subgroup' do
+ describe 'creating a top level group' do
+ let(:service) { described_class.new(user, group_params) }
+
+ context 'when user can create a group' do
+ before do
+ user.update_attribute(:can_create_group, true)
+ end
+
+ it { is_expected.to be_persisted }
+ end
+
+ context 'when user can not create a group' do
+ before do
+ user.update_attribute(:can_create_group, false)
+ end
+
+ it { is_expected.not_to be_persisted }
+ end
+ end
+
+ describe 'creating subgroup', :nested_groups do
let!(:group) { create(:group) }
let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) }
@@ -32,13 +52,38 @@ describe Groups::CreateService, '#execute' do
end
it { is_expected.to be_persisted }
+
+ context 'when nested groups feature is disabled' do
+ it 'does not save group and returns an error' do
+ allow(Group).to receive(:supports_nested_groups?).and_return(false)
+
+ is_expected.not_to be_persisted
+ expect(subject.errors[:parent_id]).to include('You don’t have permission to create a subgroup in this group.')
+ expect(subject.parent_id).to be_nil
+ end
+ end
end
- context 'as guest' do
- it 'does not save group and returns an error' do
- is_expected.not_to be_persisted
- expect(subject.errors[:parent_id].first).to eq('manage access required to create subgroup')
- expect(subject.parent_id).to be_nil
+ context 'when nested groups feature is enabled' do
+ before do
+ allow(Group).to receive(:supports_nested_groups?).and_return(true)
+ end
+
+ context 'as guest' do
+ it 'does not save group and returns an error' do
+ is_expected.not_to be_persisted
+
+ expect(subject.errors[:parent_id].first).to eq('You don’t have permission to create a subgroup in this group.')
+ expect(subject.parent_id).to be_nil
+ end
+ end
+
+ context 'as owner' do
+ before do
+ group.add_owner(user)
+ end
+
+ it { is_expected.to be_persisted }
end
end
end
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 1b2ce3cd03e..ac4b9c02ba7 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -8,8 +8,8 @@ describe Groups::DestroyService do
let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:project, namespace: group) }
let!(:notification_setting) { create(:notification_setting, source: group)}
- let!(:gitlab_shell) { Gitlab::Shell.new }
- let!(:remove_path) { group.path + "+#{group.id}+deleted" }
+ let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:remove_path) { group.path + "+#{group.id}+deleted" }
before do
group.add_user(user, Gitlab::Access::OWNER)
@@ -134,4 +134,26 @@ describe Groups::DestroyService do
it_behaves_like 'group destruction', false
end
+
+ describe 'repository removal' do
+ before do
+ destroy_group(group, user, false)
+ end
+
+ context 'legacy storage' do
+ let!(:project) { create(:project, :empty_repo, namespace: group) }
+
+ it 'removes repository' do
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey
+ end
+ end
+
+ context 'hashed storage' do
+ let!(:project) { create(:project, :hashed, :empty_repo, namespace: group) }
+
+ it 'removes repository' do
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/services/groups/nested_create_service_spec.rb b/spec/services/groups/nested_create_service_spec.rb
new file mode 100644
index 00000000000..6491fb34777
--- /dev/null
+++ b/spec/services/groups/nested_create_service_spec.rb
@@ -0,0 +1,88 @@
+require 'spec_helper'
+
+describe Groups::NestedCreateService do
+ let(:user) { create(:user) }
+
+ subject(:service) { described_class.new(user, params) }
+
+ shared_examples 'with a visibility level' do
+ it 'creates the group with correct visibility level' do
+ allow(Gitlab::CurrentSettings.current_application_settings)
+ .to receive(:default_group_visibility) { Gitlab::VisibilityLevel::INTERNAL }
+
+ group = service.execute
+
+ expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ context 'adding a visibility level ' do
+ it 'overwrites the visibility level' do
+ service = described_class.new(user, params.merge(visibility_level: Gitlab::VisibilityLevel::PRIVATE))
+
+ group = service.execute
+
+ expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+ end
+
+ describe 'without subgroups' do
+ let(:params) { { group_path: 'a-group' } }
+
+ before do
+ allow(Group).to receive(:supports_nested_groups?) { false }
+ end
+
+ it 'creates the group' do
+ group = service.execute
+
+ expect(group).to be_persisted
+ end
+
+ it 'returns the group if it already existed' do
+ existing_group = create(:group, path: 'a-group')
+
+ expect(service.execute).to eq(existing_group)
+ end
+
+ it 'raises an error when tring to create a subgroup' do
+ service = described_class.new(user, group_path: 'a-group/a-sub-group')
+
+ expect { service.execute }.to raise_error('Nested groups are not supported on MySQL')
+ end
+
+ it_behaves_like 'with a visibility level'
+ end
+
+ describe 'with subgroups', :nested_groups do
+ let(:params) { { group_path: 'a-group/a-sub-group' } }
+
+ describe "#execute" do
+ it 'returns the group if it already existed' do
+ parent = create(:group, path: 'a-group', owner: user)
+ child = create(:group, path: 'a-sub-group', parent: parent, owner: user)
+
+ expect(service.execute).to eq(child)
+ end
+
+ it 'reuses a parent if it already existed' do
+ parent = create(:group, path: 'a-group')
+ parent.add_owner(user)
+
+ expect(service.execute.parent).to eq(parent)
+ end
+
+ it 'creates group and subgroup in the database' do
+ service.execute
+
+ parent = Group.find_by_full_path('a-group')
+ child = parent.children.find_by(path: 'a-sub-group')
+
+ expect(parent).not_to be_nil
+ expect(child).not_to be_nil
+ end
+
+ it_behaves_like 'with a visibility level'
+ end
+ end
+end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 44f22a3b37b..1737fd0a9fc 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -100,4 +100,38 @@ describe Groups::UpdateService do
end
end
end
+
+ context 'for a subgroup', :nested_groups do
+ let(:subgroup) { create(:group, :private, parent: private_group) }
+
+ context 'when the parent group share_with_group_lock is enabled' do
+ before do
+ private_group.update_column(:share_with_group_lock, true)
+ end
+
+ context 'for the parent group owner' do
+ it 'allows disabling share_with_group_lock' do
+ private_group.add_owner(user)
+
+ result = described_class.new(subgroup, user, share_with_group_lock: false).execute
+
+ expect(result).to be_truthy
+ expect(subgroup.reload.share_with_group_lock).to be_falsey
+ end
+ end
+
+ context 'for a subgroup owner (who does not own the parent)' do
+ it 'does not allow disabling share_with_group_lock' do
+ subgroup_owner = create(:user)
+ subgroup.add_owner(subgroup_owner)
+
+ result = described_class.new(subgroup, subgroup_owner, share_with_group_lock: false).execute
+
+ expect(result).to be_falsey
+ expect(subgroup.errors.full_messages.first).to match(/cannot be disabled when the parent group "Share with group lock" is enabled, except by the owner of the parent group/)
+ expect(subgroup.reload.share_with_group_lock).to be_truthy
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index befa65ec0f8..bdaab88d673 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -118,7 +118,7 @@ describe Issuable::BulkUpdateService do
context 'when the new assignee ID is not present' do
it 'does not unassign' do
expect { bulk_update(issue, assignee_ids: []) }
- .not_to change{ issue.reload.assignees }
+ .not_to change { issue.reload.assignees }
end
end
end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index a03f68434de..171f70c32a8 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -42,6 +42,11 @@ describe Issues::CloseService do
service.execute(issue)
end
+ it 'refreshes the number of open issues' do
+ expect { service.execute(issue) }
+ .to change { project.open_issues_count }.from(1).to(0)
+ end
+
it 'invalidates counter cache for assignees' do
expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts)
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index fcbd69fd58b..cc3d648c340 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -35,6 +35,10 @@ describe Issues::CreateService do
expect(issue.due_date).to eq Date.tomorrow
end
+ it 'refreshes the number of open issues' do
+ expect { issue }.to change { project.open_issues_count }.from(0).to(1)
+ end
+
context 'when current user cannot admin issues in the project' do
let(:guest) { create(:user) }
@@ -351,14 +355,14 @@ describe Issues::CreateService do
end
it 'marks related spam_log as recaptcha_verified' do
- expect { issue }.to change{SpamLog.last.recaptcha_verified}.from(false).to(true)
+ expect { issue }.to change {SpamLog.last.recaptcha_verified}.from(false).to(true)
end
context 'when spam log does not belong to a user' do
let(:log_user) { create(:user) }
it 'does not mark spam_log as recaptcha_verified' do
- expect { issue }.not_to change{SpamLog.last.recaptcha_verified}
+ expect { issue }.not_to change {SpamLog.last.recaptcha_verified}
end
end
end
@@ -366,7 +370,7 @@ describe Issues::CreateService do
context 'when recaptcha was not verified' do
context 'when akismet detects spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
end
it 'marks an issue as a spam ' do
@@ -378,7 +382,7 @@ describe Issues::CreateService do
end
it 'creates a new spam_log' do
- expect{issue}.to change{SpamLog.count}.from(0).to(1)
+ expect {issue}.to change {SpamLog.count}.from(0).to(1)
end
it 'assigns a spam_log to an issue' do
@@ -388,7 +392,7 @@ describe Issues::CreateService do
context 'when akismet does not detect spam' do
before do
- allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(false)
end
it 'does not mark an issue as a spam ' do
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 205e9ebd237..48fc98b3b2f 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -34,6 +34,13 @@ describe Issues::ReopenService do
described_class.new(project, user).execute(issue)
end
+ it 'refreshes the number of opened issues' do
+ service = described_class.new(project, user)
+
+ expect { service.execute(issue) }
+ .to change { project.open_issues_count }.from(0).to(1)
+ end
+
context 'when issue is not confidential' do
it 'executes issue hooks' do
expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
index fac66791ffb..67a86c50fc1 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -20,7 +20,7 @@ describe Issues::ResolveDiscussions do
describe "for resolving discussions" do
let(:discussion) { create(:diff_note_on_merge_request, project: project, note: "Almost done").to_discussion }
let(:merge_request) { discussion.noteable }
- let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "other") }
+ let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "fix") }
describe "#merge_request_for_resolving_discussion" do
let(:service) { DummyService.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) }
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 814e2cfbed0..15a50b85f19 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -80,7 +80,7 @@ describe Issues::UpdateService, :mailer do
issue.save
end
- opts[:move_between_iids] = [issue1.iid, issue2.iid]
+ opts[:move_between_ids] = [issue1.id, issue2.id]
update_issue(opts)
@@ -478,7 +478,7 @@ describe Issues::UpdateService, :mailer do
feature_visibility_attr = :"#{issue.model_name.plural}_access_level"
project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
- expect{ update_issue(assignee_ids: [assignee.id]) }.not_to change{ issue.assignees }
+ expect { update_issue(assignee_ids: [assignee.id]) }.not_to change { issue.assignees }
end
end
end
@@ -510,6 +510,26 @@ describe Issues::UpdateService, :mailer do
end
end
+ context 'move issue to another project' do
+ let(:target_project) { create(:project) }
+
+ context 'valid project' do
+ before do
+ target_project.team << [user, :master]
+ end
+
+ it 'calls the move service with the proper issue and project' do
+ move_stub = instance_double(Issues::MoveService)
+ allow(Issues::MoveService).to receive(:new).and_return(move_stub)
+ allow(move_stub).to receive(:execute).with(issue, target_project).and_return(issue)
+
+ expect(move_stub).to receive(:execute).with(issue, target_project)
+
+ update_issue(target_project: target_project)
+ end
+ end
+ end
+
include_examples 'issuable update service' do
let(:open_issuable) { issue }
let(:closed_issuable) { create(:closed_issue, project: project) }
diff --git a/spec/services/labels/update_service_spec.rb b/spec/services/labels/update_service_spec.rb
index bb95fe20fbf..c3fe33045fa 100644
--- a/spec/services/labels/update_service_spec.rb
+++ b/spec/services/labels/update_service_spec.rb
@@ -13,7 +13,7 @@ describe Labels::UpdateService do
let(:expected_saved_color) { hex_color }
- before(:each) do
+ before do
@label = Labels::CreateService.new(title: 'Initial', color: '#000000').execute(project: project)
expect(@label).to be_persisted
end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 04bf267d167..7e65369762c 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -52,6 +52,13 @@ describe MergeRequests::CloseService do
end
end
+ it 'refreshes the number of open merge requests for a valid MR' do
+ service = described_class.new(project, user, {})
+
+ expect { service.execute(merge_request) }
+ .to change { project.open_merge_requests_count }.from(1).to(0)
+ end
+
context 'current user is not authorized to close merge request' do
before do
perform_enqueued_jobs do
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index 492b55cdece..313f87ae1f6 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -2,8 +2,10 @@ require 'spec_helper'
describe MergeRequests::CreateFromIssueService do
let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
- let(:issue) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+ let(:label_ids) { create_pair(:label, project: project).map(&:id) }
+ let(:milestone_id) { create(:milestone, project: project).id }
+ let(:issue) { create(:issue, project: project, milestone_id: milestone_id) }
subject(:service) { described_class.new(project, user, issue_iid: issue.iid) }
@@ -25,6 +27,20 @@ describe MergeRequests::CreateFromIssueService do
described_class.new(project, user, issue_iid: -1).execute
end
+ it "inherits labels" do
+ issue.assign_attributes(label_ids: label_ids)
+
+ result = service.execute
+
+ expect(result[:merge_request].label_ids).to eq(label_ids)
+ end
+
+ it "inherits milestones" do
+ result = service.execute
+
+ expect(result[:merge_request].milestone_id).to eq(milestone_id)
+ end
+
it 'delegates the branch creation to CreateBranchService' do
expect_any_instance_of(CreateBranchService).to receive(:execute).once.and_call_original
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 8fef480274d..d6409c0d625 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -18,36 +18,50 @@ describe MergeRequests::CreateService do
end
let(:service) { described_class.new(project, user, opts) }
+ let(:merge_request) { service.execute }
before do
project.team << [user, :master]
project.team << [assignee, :developer]
allow(service).to receive(:execute_hooks)
-
- @merge_request = service.execute
end
it 'creates an MR' do
- expect(@merge_request).to be_valid
- expect(@merge_request.title).to eq('Awesome merge_request')
- expect(@merge_request.assignee).to be_nil
- expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1')
+ expect(merge_request).to be_valid
+ expect(merge_request.title).to eq('Awesome merge_request')
+ expect(merge_request.assignee).to be_nil
+ expect(merge_request.merge_params['force_remove_source_branch']).to eq('1')
end
it 'executes hooks with default action' do
- expect(service).to have_received(:execute_hooks).with(@merge_request)
+ expect(service).to have_received(:execute_hooks).with(merge_request)
+ end
+
+ it 'refreshes the number of open merge requests' do
+ expect { service.execute }
+ .to change { project.open_merge_requests_count }.from(0).to(1)
end
it 'does not creates todos' do
attributes = {
project: project,
- target_id: @merge_request.id,
- target_type: @merge_request.class.name
+ target_id: merge_request.id,
+ target_type: merge_request.class.name
}
expect(Todo.where(attributes).count).to be_zero
end
+ it 'creates exactly 1 create MR event' do
+ attributes = {
+ action: Event::CREATED,
+ target_id: merge_request.id,
+ target_type: merge_request.class.name
+ }
+
+ expect(Event.where(attributes).count).to eq(1)
+ end
+
context 'when merge request is assigned to someone' do
let(:opts) do
{
@@ -59,15 +73,15 @@ describe MergeRequests::CreateService do
}
end
- it { expect(@merge_request.assignee).to eq assignee }
+ it { expect(merge_request.assignee).to eq assignee }
it 'creates a todo for new assignee' do
attributes = {
project: project,
author: user,
user: assignee,
- target_id: @merge_request.id,
- target_type: @merge_request.class.name,
+ target_id: merge_request.id,
+ target_type: merge_request.class.name,
action: Todo::ASSIGNED,
state: :pending
}
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 672d86e4028..25599dea19f 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -3,7 +3,7 @@ require "spec_helper"
describe MergeRequests::GetUrlsService do
let(:project) { create(:project, :public, :repository) }
let(:service) { described_class.new(project) }
- let(:source_branch) { "my_branch" }
+ let(:source_branch) { "merge-test" }
let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" }
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
@@ -111,9 +111,9 @@ describe MergeRequests::GetUrlsService do
end
context 'pushing new branch and existing branch (with merge request created) at once' do
- let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "existing_branch") }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "markdown") }
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" }
- let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" }
+ let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/markdown" }
let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" }
let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" }
@@ -124,7 +124,7 @@ describe MergeRequests::GetUrlsService do
url: new_merge_request_url,
new_merge_request: true
}, {
- branch_name: "existing_branch",
+ branch_name: "markdown",
url: show_merge_request_url,
new_merge_request: false
}])
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index e593bfeeaf7..b60136064b7 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -12,6 +12,38 @@ describe MergeRequests::MergeService do
end
describe '#execute' do
+ context 'MergeRequest#merge_jid' do
+ 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)
+
+ 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)
+
+ expect { service.execute(merge_request) }.to raise_error(StandardError)
+
+ expect(merge_request.reload.merge_jid).to be_present
+ end
+ end
+
context 'valid params' do
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
@@ -217,7 +249,7 @@ describe MergeRequests::MergeService do
it 'logs and saves error if there is an PreReceiveError exception' do
error_message = 'error message'
- allow(service).to receive(:repository).and_raise(GitHooksService::PreReceiveError, 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)
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index f02af0c582e..fa652611c6b 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -47,6 +47,13 @@ describe MergeRequests::ReopenService do
end
end
+ it 'refreshes the number of open merge requests for a valid MR' do
+ service = described_class.new(project, user, {})
+
+ expect { service.execute(merge_request) }
+ .to change { project.open_merge_requests_count }.from(0).to(1)
+ end
+
context 'current user is not authorized to reopen merge request' do
before do
perform_enqueued_jobs do
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 9368594bc86..681feee61d1 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -509,7 +509,7 @@ describe MergeRequests::UpdateService, :mailer do
feature_visibility_attr = :"#{merge_request.model_name.plural}_access_level"
project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
- expect{ update_merge_request(assignee_id: assignee) }.not_to change{ merge_request.assignee }
+ expect { update_merge_request(assignee_id: assignee) }.not_to change { merge_request.assignee }
end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 6af5c79135d..3e493148b32 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -4,7 +4,7 @@ describe NotificationService, :mailer do
let(:notification) { described_class.new }
let(:assignee) { create(:user) }
- around(:each) do |example|
+ around do |example|
perform_enqueued_jobs do
example.run
end
@@ -80,12 +80,16 @@ describe NotificationService, :mailer do
describe 'Keys' do
describe '#new_key' do
- let!(:key) { create(:personal_key) }
+ let(:key_options) { {} }
+ let!(:key) { create(:personal_key, key_options) }
it { expect(notification.new_key(key)).to be_truthy }
+ it { should_email(key.user) }
- it 'sends email to key owner' do
- expect{ notification.new_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
+ describe 'never emails the ghost user' do
+ let(:key_options) { { user: User.ghost } }
+
+ it { should_not_email_anyone }
end
end
end
@@ -97,7 +101,7 @@ describe NotificationService, :mailer do
it { expect(notification.new_gpg_key(key)).to be_truthy }
it 'sends email to key owner' do
- expect{ notification.new_gpg_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
+ expect { notification.new_gpg_key(key) }.to change { ActionMailer::Base.deliveries.size }.by(1)
end
end
end
@@ -109,7 +113,7 @@ describe NotificationService, :mailer do
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)
+ expect { notification.new_email(email) }.to change { ActionMailer::Base.deliveries.size }.by(1)
end
end
end
@@ -126,7 +130,18 @@ describe NotificationService, :mailer do
project.add_master(issue.author)
project.add_master(assignee)
project.add_master(note.author)
- create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy')
+
+ @u_custom_off = create_user_with_notification(:custom, 'custom_off')
+ project.add_guest(@u_custom_off)
+
+ create(
+ :note_on_issue,
+ author: @u_custom_off,
+ noteable: issue,
+ project_id: issue.project_id,
+ note: 'i think @subscribed_participant should see this'
+ )
+
update_custom_notification(:new_note, @u_guest_custom, resource: project)
update_custom_notification(:new_note, @u_custom_global)
end
@@ -136,8 +151,7 @@ describe NotificationService, :mailer do
add_users_with_subscription(note.project, issue)
reset_delivered_emails!
- # Ensure create SentNotification by noteable = issue 6 times, not noteable = note
- expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times
+ expect(SentNotification).to receive(:record).with(issue, any_args).exactly(9).times
notification.new_note(note)
@@ -149,6 +163,7 @@ describe NotificationService, :mailer do
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_email(@subscribed_participant)
+ should_email(@u_custom_off)
should_not_email(@u_guest_custom)
should_not_email(@u_guest_watcher)
should_not_email(note.author)
@@ -1173,19 +1188,39 @@ describe NotificationService, :mailer do
end
end
- describe '#project_exported' do
- it do
- notification.project_exported(project, @u_disabled)
+ context 'user with notifications disabled' do
+ describe '#project_exported' do
+ it do
+ notification.project_exported(project, @u_disabled)
- should_only_email(@u_disabled)
+ should_not_email_anyone
+ end
+ end
+
+ describe '#project_not_exported' do
+ it do
+ notification.project_not_exported(project, @u_disabled, ['error'])
+
+ should_not_email_anyone
+ end
end
end
- describe '#project_not_exported' do
- it do
- notification.project_not_exported(project, @u_disabled, ['error'])
+ context 'user with notifications enabled' do
+ describe '#project_exported' do
+ it do
+ notification.project_exported(project, @u_participating)
- should_only_email(@u_disabled)
+ should_only_email(@u_participating)
+ end
+ end
+
+ describe '#project_not_exported' do
+ it do
+ notification.project_not_exported(project, @u_participating, ['error'])
+
+ should_only_email(@u_participating)
+ end
end
end
end
@@ -1196,19 +1231,48 @@ describe NotificationService, :mailer do
let(:group) { create(:group) }
let(:member) { create(:user) }
- before(:each) do
+ before do
group.add_owner(creator)
group.add_developer(member, creator)
end
it do
- group_member = group.members.first
+ group_member = group.members.last
expect do
notification.decline_group_invite(group_member)
end.to change { ActionMailer::Base.deliveries.size }.by(1)
end
end
+
+ describe '#new_group_member' do
+ let(:group) { create(:group) }
+ let(:added_user) { create(:user) }
+
+ def create_member!
+ GroupMember.create(
+ group: group,
+ user: added_user,
+ access_level: Gitlab::Access::GUEST
+ )
+ end
+
+ it 'sends a notification' do
+ create_member!
+ should_only_email(added_user)
+ end
+
+ describe 'when notifications are disabled' do
+ before do
+ create_global_setting_for(added_user, :disabled)
+ end
+
+ it 'does not send a notification' do
+ create_member!
+ should_not_email_anyone
+ end
+ end
+ end
end
describe 'ProjectMember' do
@@ -1216,18 +1280,43 @@ describe NotificationService, :mailer do
let(:project) { create(:project) }
let(:member) { create(:user) }
- before(:each) do
+ before do
project.add_developer(member, current_user: project.owner)
end
it do
- project_member = project.members.first
+ project_member = project.members.last
expect do
notification.decline_project_invite(project_member)
end.to change { ActionMailer::Base.deliveries.size }.by(1)
end
end
+
+ describe '#new_project_member' do
+ let(:project) { create(:project) }
+ let(:added_user) { create(:user) }
+
+ def create_member!
+ create(:project_member, user: added_user, project: project)
+ end
+
+ it do
+ create_member!
+ should_only_email(added_user)
+ end
+
+ describe 'when notifications are disabled' do
+ before do
+ create_global_setting_for(added_user, :disabled)
+ end
+
+ it do
+ create_member!
+ should_not_email_anyone
+ end
+ end
+ end
end
context 'guest user in private project' do
diff --git a/spec/services/projects/after_import_service_spec.rb b/spec/services/projects/after_import_service_spec.rb
new file mode 100644
index 00000000000..c6678fc1f5c
--- /dev/null
+++ b/spec/services/projects/after_import_service_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Projects::AfterImportService do
+ subject { described_class.new(project) }
+
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:sha) { project.commit.sha }
+ let(:housekeeping_service) { double(:housekeeping_service) }
+
+ describe '#execute' do
+ before do
+ allow(Projects::HousekeepingService)
+ .to receive(:new).with(project).and_return(housekeeping_service)
+
+ allow(housekeeping_service)
+ .to receive(:execute).and_yield
+ end
+
+ it 'performs housekeeping' do
+ subject.execute
+
+ expect(housekeeping_service).to have_received(:execute)
+ end
+
+ context 'with some refs in refs/pull/**/*' do
+ before do
+ repository.write_ref('refs/pull/1/head', sha)
+ repository.write_ref('refs/pull/1/merge', sha)
+
+ subject.execute
+ end
+
+ it 'removes refs/pull/**/*' do
+ expect(repository.rugged.references.map(&:name))
+ .not_to include(%r{\Arefs/pull/})
+ end
+ end
+
+ Repository::RESERVED_REFS_NAMES.each do |name|
+ context "with a ref in refs/#{name}/tmp" do
+ before do
+ repository.write_ref("refs/#{name}/tmp", sha)
+
+ subject.execute
+ end
+
+ it "does not remove refs/#{name}/tmp" do
+ expect(repository.rugged.references.map(&:name))
+ .to include("refs/#{name}/tmp")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/count_service_spec.rb b/spec/services/projects/count_service_spec.rb
new file mode 100644
index 00000000000..79b01e7620e
--- /dev/null
+++ b/spec/services/projects/count_service_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Projects::CountService do
+ let(:project) { build(:project, id: 1) }
+ let(:service) { described_class.new(project) }
+
+ describe '#relation_for_count' do
+ it 'raises NotImplementedError' do
+ expect { service.relation_for_count }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#count' do
+ before do
+ allow(service).to receive(:cache_key_name).and_return('count_service')
+ end
+
+ it 'returns the number of rows' do
+ allow(service).to receive(:uncached_count).and_return(1)
+
+ expect(service.count).to eq(1)
+ end
+
+ it 'caches the number of rows', :use_clean_rails_memory_store_caching do
+ expect(service).to receive(:uncached_count).once.and_return(1)
+
+ 2.times do
+ expect(service.count).to eq(1)
+ end
+ end
+ end
+
+ describe '#refresh_cache', :use_clean_rails_memory_store_caching do
+ before do
+ allow(service).to receive(:cache_key_name).and_return('count_service')
+ end
+
+ it 'refreshes the cache' do
+ expect(service).to receive(:uncached_count).once.and_return(1)
+
+ service.refresh_cache
+
+ expect(service.count).to eq(1)
+ end
+ end
+
+ describe '#delete_cache', :use_clean_rails_memory_store_caching do
+ before do
+ allow(service).to receive(:cache_key_name).and_return('count_service')
+ end
+
+ it 'removes the cache' do
+ expect(service).to receive(:uncached_count).twice.and_return(1)
+
+ service.count
+ service.delete_cache
+ service.count
+ end
+ end
+
+ describe '#cache_key_name' do
+ it 'raises NotImplementedError' do
+ expect { service.cache_key_name }.to raise_error(NotImplementedError)
+ end
+ end
+
+ 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'])
+ end
+ end
+end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index b0dc7488b5f..5da634e2fb1 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -1,11 +1,12 @@
require 'spec_helper'
describe Projects::CreateService, '#execute' do
+ let(:gitlab_shell) { Gitlab::Shell.new }
let(:user) { create :user }
let(:opts) do
{
- name: "GitLab",
- namespace: user.namespace
+ name: 'GitLab',
+ namespace_id: user.namespace.id
}
end
@@ -38,7 +39,7 @@ describe Projects::CreateService, '#execute' do
expect(project).to be_persisted
expect(project.owner).to eq(user)
- expect(project.team.masters).to include(user, admin)
+ expect(project.team.masters).to contain_exactly(user)
expect(project.namespace).to eq(user.namespace)
end
end
@@ -146,6 +147,41 @@ describe Projects::CreateService, '#execute' do
expect(project.owner).to eq(user)
expect(project.namespace).to eq(user.namespace)
end
+
+ context 'when another repository already exists on disk' do
+ let(:opts) do
+ {
+ name: 'Existing',
+ namespace_id: user.namespace.id
+ }
+ end
+
+ let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] }
+
+ 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
+
+ it 'does not allow to create project with same path' do
+ project = create_project(user, opts)
+
+ 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
+
+ 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' }))
+
+ 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
context 'when there is an active service template' do
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 85b05ef6d05..c867139d1de 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -142,13 +142,13 @@ describe Projects::DestroyService do
context 'when `execute` raises unexpected error' do
before do
expect_any_instance_of(Project)
- .to receive(:destroy!).and_raise(Exception.new("Other error message"))
+ .to receive(:destroy!).and_raise(Exception.new('Other error message'))
end
it 'allows error to bubble up and rolls back project deletion' do
expect do
Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
- end.to raise_error
+ end.to raise_error(Exception, 'Other error message')
expect(project.reload.pending_delete).to be(false)
expect(project.delete_error).to include("Other error message")
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index c90536ba346..a6e0364d44c 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Projects::ForkService do
+ let(:gitlab_shell) { Gitlab::Shell.new }
+
describe 'fork by user' do
before do
@from_user = create(:user)
@@ -50,6 +52,14 @@ describe Projects::ForkService do
expect(@from_project.avatar.file).to be_exists
end
+
+ it 'flushes the forks count cache of the source project' do
+ expect(@from_project.forks_count).to be_zero
+
+ fork_project(@from_project, @to_user)
+
+ expect(@from_project.forks_count).to eq(1)
+ end
end
end
@@ -65,6 +75,26 @@ describe Projects::ForkService do
end
end
+ context 'repository already exists' do
+ let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] }
+
+ before do
+ gitlab_shell.add_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}")
+ end
+
+ after do
+ gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}")
+ end
+
+ it 'does not allow creation' do
+ to_project = fork_project(@from_project, @to_user)
+
+ expect(to_project).not_to be_persisted
+ expect(to_project.errors.messages).to have_key(:base)
+ expect(to_project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ end
+ end
+
context 'GitLab CI is enabled' do
it "forks and enables CI for fork" do
@from_project.enable_ci
diff --git a/spec/services/projects/forks_count_service_spec.rb b/spec/services/projects/forks_count_service_spec.rb
new file mode 100644
index 00000000000..9f8e7ee18a8
--- /dev/null
+++ b/spec/services/projects/forks_count_service_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe Projects::ForksCountService do
+ describe '#count' do
+ it 'returns the number of forks' do
+ project = build(:project, id: 42)
+ service = described_class.new(project)
+
+ allow(service).to receive(:uncached_count).and_return(1)
+
+ expect(service.count).to eq(1)
+ end
+ end
+end
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index ebed802708d..437c009e7fa 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -20,11 +20,18 @@ describe Projects::HousekeepingService do
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :the_task, :the_lease_key, :the_uuid)
subject.execute
+
expect(project.reload.pushes_since_gc).to eq(0)
end
+ it 'yields the block if given' do
+ expect do |block|
+ subject.execute(&block)
+ end.to yield_with_no_args
+ end
+
context 'when no lease can be obtained' do
- before(:each) do
+ before do
expect(subject).to receive(:try_obtain_lease).and_return(false)
end
@@ -39,6 +46,13 @@ describe Projects::HousekeepingService do
expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
end.not_to change { project.pushes_since_gc }
end
+
+ it 'does not yield' do
+ expect do |block|
+ expect { subject.execute(&block) }
+ .to raise_error(Projects::HousekeepingService::LeaseTaken)
+ end.not_to yield_with_no_args
+ end
end
end
diff --git a/spec/services/projects/open_issues_count_service_spec.rb b/spec/services/projects/open_issues_count_service_spec.rb
new file mode 100644
index 00000000000..f964f9972cd
--- /dev/null
+++ b/spec/services/projects/open_issues_count_service_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Projects::OpenIssuesCountService do
+ describe '#count' do
+ it 'returns the number of open issues' do
+ project = create(:project)
+ create(:issue, :opened, project: project)
+
+ expect(described_class.new(project).count).to eq(1)
+ end
+
+ it 'does not include confidential issues in the issue count' do
+ project = create(:project)
+
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
+
+ expect(described_class.new(project).count).to eq(1)
+ end
+ end
+end
diff --git a/spec/services/projects/open_merge_requests_count_service_spec.rb b/spec/services/projects/open_merge_requests_count_service_spec.rb
new file mode 100644
index 00000000000..9f49b9ec6a2
--- /dev/null
+++ b/spec/services/projects/open_merge_requests_count_service_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe Projects::OpenMergeRequestsCountService do
+ describe '#count' do
+ it 'returns the number of open merge requests' do
+ project = create(:project)
+ create(:merge_request,
+ :opened,
+ source_project: project,
+ target_project: project)
+
+ expect(described_class.new(project).count).to eq(1)
+ end
+ end
+end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 2cb60cbcfc4..a14ed526f68 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Projects::TransferService do
+ let(:gitlab_shell) { Gitlab::Shell.new }
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
@@ -119,6 +120,25 @@ describe Projects::TransferService do
it { expect(project.namespace).to eq(user.namespace) }
end
+ context 'namespace which contains orphan repository with same projects path name' do
+ let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] }
+
+ before do
+ group.add_owner(user)
+ gitlab_shell.add_repository(repository_storage_path, "#{group.full_path}/#{project.path}")
+
+ @result = transfer_project(project, user, group)
+ end
+
+ after do
+ gitlab_shell.remove_repository(repository_storage_path, "#{group.full_path}/#{project.path}")
+ end
+
+ it { expect(@result).to eq false }
+ it { expect(project.namespace).to eq(user.namespace) }
+ it { expect(project.errors[:new_namespace]).to include('Cannot move project') }
+ end
+
def transfer_project(project, user, new_namespace)
service = Projects::TransferService.new(project, user)
diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb
index 2ae8d5f7c54..4f1ab697460 100644
--- a/spec/services/projects/unlink_fork_service_spec.rb
+++ b/spec/services/projects/unlink_fork_service_spec.rb
@@ -29,4 +29,14 @@ describe Projects::UnlinkForkService do
subject.execute
end
+
+ it 'refreshes the forks count cache of the source project' do
+ source = fork_project.forked_from_project
+
+ expect(source.forks_count).to eq(1)
+
+ subject.execute
+
+ expect(source.forks_count).to be_zero
+ end
end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index aa6ad6340f5..031366d1825 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -116,6 +116,7 @@ describe Projects::UpdatePagesService do
expect(deploy_status.description)
.to match(/artifacts for pages are too large/)
+ expect(deploy_status).to be_script_failure
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index d945e0aa1f0..92cc9a37795 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Projects::UpdateService, '#execute' do
+ let(:gitlab_shell) { Gitlab::Shell.new }
let(:user) { create(:user) }
let(:admin) { create(:admin) }
@@ -101,6 +102,13 @@ describe Projects::UpdateService, '#execute' do
expect(Project.find(project.id).default_branch).to eq 'feature'
end
+
+ it 'does not change a default branch' do
+ # The branch 'unexisted-branch' does not exist.
+ update_project(project, admin, default_branch: 'unexisted-branch')
+
+ expect(Project.find(project.id).default_branch).to eq 'master'
+ end
end
context 'when updating a project that contains container images' do
@@ -125,6 +133,28 @@ describe Projects::UpdateService, '#execute' do
end
end
+ context 'when renaming a project' do
+ let(:repository_storage_path) { Gitlab.config.repositories.storages['default']['path'] }
+
+ 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
+
+ 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('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')
+ end
+ end
+
context 'when passing invalid parameters' do
it 'returns an error result when record cannot be updated' do
result = update_project(project, admin, { name: 'foo&bar' })
diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb
index 5698101af54..9fa5983db66 100644
--- a/spec/services/protected_branches/update_service_spec.rb
+++ b/spec/services/protected_branches/update_service_spec.rb
@@ -19,7 +19,7 @@ describe ProtectedBranches::UpdateService do
let(:user) { create(:user) }
it "raises error" do
- expect{ service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
diff --git a/spec/services/protected_tags/update_service_spec.rb b/spec/services/protected_tags/update_service_spec.rb
index d333430088d..2ece4e3b07b 100644
--- a/spec/services/protected_tags/update_service_spec.rb
+++ b/spec/services/protected_tags/update_service_spec.rb
@@ -19,7 +19,7 @@ describe ProtectedTags::UpdateService do
let(:user) { create(:user) }
it "raises error" do
- expect{ service.execute(protected_tag) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect { service.execute(protected_tag) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
diff --git a/spec/services/push_event_payload_service_spec.rb b/spec/services/push_event_payload_service_spec.rb
new file mode 100644
index 00000000000..81956200bff
--- /dev/null
+++ b/spec/services/push_event_payload_service_spec.rb
@@ -0,0 +1,218 @@
+require 'spec_helper'
+
+describe PushEventPayloadService do
+ let(:event) { create(:push_event) }
+
+ describe '#execute' do
+ let(:push_data) do
+ {
+ commits: [
+ {
+ id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ message: 'This is a commit'
+ }
+ ],
+ before: '0000000000000000000000000000000000000000',
+ after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2',
+ total_commits_count: 1,
+ ref: 'refs/heads/my-branch'
+ }
+ end
+
+ it 'creates a new PushEventPayload row' do
+ payload = described_class.new(event, push_data).execute
+
+ expect(payload.commit_count).to eq(1)
+ expect(payload.action).to eq('created')
+ expect(payload.ref_type).to eq('branch')
+ expect(payload.commit_from).to be_nil
+ expect(payload.commit_to).to eq(push_data[:after])
+ expect(payload.ref).to eq('my-branch')
+ expect(payload.commit_title).to eq('This is a commit')
+ expect(payload.event_id).to eq(event.id)
+ end
+
+ it 'sets the push_event_payload association of the used event' do
+ payload = described_class.new(event, push_data).execute
+
+ expect(event.push_event_payload).to eq(payload)
+ end
+ end
+
+ describe '#commit_title' do
+ it 'returns nil if no commits were pushed' do
+ service = described_class.new(event, commits: [])
+
+ expect(service.commit_title).to be_nil
+ end
+
+ it 'returns a String limited to 70 characters' do
+ service = described_class.new(event, commits: [{ message: 'a' * 100 }])
+
+ expect(service.commit_title).to eq(('a' * 67) + '...')
+ end
+
+ it 'does not truncate the commit message if it is shorter than 70 characters' do
+ service = described_class.new(event, commits: [{ message: 'Hello' }])
+
+ expect(service.commit_title).to eq('Hello')
+ end
+
+ it 'includes the first line of a commit message if the message spans multiple lines' do
+ service = described_class
+ .new(event, commits: [{ message: "Hello\n\nworld" }])
+
+ expect(service.commit_title).to eq('Hello')
+ end
+ end
+
+ describe '#commit_from_id' do
+ it 'returns nil when creating a new ref' do
+ service = described_class.new(event, before: Gitlab::Git::BLANK_SHA)
+
+ expect(service.commit_from_id).to be_nil
+ end
+
+ it 'returns the ID of the first commit when pushing to an existing ref' do
+ service = described_class.new(event, before: '123')
+
+ expect(service.commit_from_id).to eq('123')
+ end
+ end
+
+ describe '#commit_to_id' do
+ it 'returns nil when removing an existing ref' do
+ service = described_class.new(event, after: Gitlab::Git::BLANK_SHA)
+
+ expect(service.commit_to_id).to be_nil
+ end
+ end
+
+ describe '#commit_count' do
+ it 'returns the number of commits' do
+ service = described_class.new(event, total_commits_count: 1)
+
+ expect(service.commit_count).to eq(1)
+ end
+
+ it 'raises when the push data does not contain the commits count' do
+ service = described_class.new(event, {})
+
+ expect { service.commit_count }.to raise_error(KeyError)
+ end
+ end
+
+ describe '#ref' do
+ it 'returns the name of the ref' do
+ service = described_class.new(event, ref: 'refs/heads/foo')
+
+ expect(service.ref).to eq('refs/heads/foo')
+ end
+
+ it 'raises when the push data does not contain the ref name' do
+ service = described_class.new(event, {})
+
+ expect { service.ref }.to raise_error(KeyError)
+ end
+ end
+
+ describe '#revision_before' do
+ it 'returns the revision from before the push' do
+ service = described_class.new(event, before: 'foo')
+
+ expect(service.revision_before).to eq('foo')
+ end
+
+ it 'raises when the push data does not contain the before revision' do
+ service = described_class.new(event, {})
+
+ expect { service.revision_before }.to raise_error(KeyError)
+ end
+ end
+
+ describe '#revision_after' do
+ it 'returns the revision from after the push' do
+ service = described_class.new(event, after: 'foo')
+
+ expect(service.revision_after).to eq('foo')
+ end
+
+ it 'raises when the push data does not contain the after revision' do
+ service = described_class.new(event, {})
+
+ expect { service.revision_after }.to raise_error(KeyError)
+ end
+ end
+
+ describe '#trimmed_ref' do
+ it 'returns the ref name without its prefix' do
+ service = described_class.new(event, ref: 'refs/heads/foo')
+
+ expect(service.trimmed_ref).to eq('foo')
+ end
+ end
+
+ describe '#create?' do
+ it 'returns true when creating a new ref' do
+ service = described_class.new(event, before: Gitlab::Git::BLANK_SHA)
+
+ expect(service.create?).to eq(true)
+ end
+
+ it 'returns false when pushing to an existing ref' do
+ service = described_class.new(event, before: 'foo')
+
+ expect(service.create?).to eq(false)
+ end
+ end
+
+ describe '#remove?' do
+ it 'returns true when removing an existing ref' do
+ service = described_class.new(event, after: Gitlab::Git::BLANK_SHA)
+
+ expect(service.remove?).to eq(true)
+ end
+
+ it 'returns false pushing to an existing ref' do
+ service = described_class.new(event, after: 'foo')
+
+ expect(service.remove?).to eq(false)
+ end
+ end
+
+ describe '#action' do
+ it 'returns :created when creating a ref' do
+ service = described_class.new(event, before: Gitlab::Git::BLANK_SHA)
+
+ expect(service.action).to eq(:created)
+ end
+
+ it 'returns :removed when removing an existing ref' do
+ service = described_class.new(event,
+ before: '123',
+ after: Gitlab::Git::BLANK_SHA)
+
+ expect(service.action).to eq(:removed)
+ end
+
+ it 'returns :pushed when pushing to an existing ref' do
+ service = described_class.new(event, before: '123', after: '456')
+
+ expect(service.action).to eq(:pushed)
+ end
+ end
+
+ describe '#ref_type' do
+ it 'returns :tag for a tag' do
+ service = described_class.new(event, ref: 'refs/tags/1.2')
+
+ expect(service.ref_type).to eq(:tag)
+ end
+
+ it 'returns :branch for a branch' do
+ service = described_class.new(event, ref: 'refs/heads/master')
+
+ expect(service.ref_type).to eq(:branch)
+ end
+ end
+end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 30fa0ee6873..6926ac85de3 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1147,5 +1147,15 @@ describe QuickActions::InterpretService do
expect(explanations).to eq(["Moves issue to ~#{bug.id} column in the board."])
end
end
+
+ describe 'move issue to another project command' do
+ let(:content) { '/move test/project' }
+
+ it 'includes the project name' do
+ _, explanations = service.explain(content, issue)
+
+ expect(explanations).to eq(["Moves this issue to test/project."])
+ end
+ end
end
end
diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb
index a14dfa3f01f..61312d55b84 100644
--- a/spec/services/spam_service_spec.rb
+++ b/spec/services/spam_service_spec.rb
@@ -23,7 +23,7 @@ describe SpamService do
before do
issue.closed_at = Time.zone.now
- allow(AkismetService).to receive(:new).and_return(double(is_spam?: true))
+ allow(AkismetService).to receive(:new).and_return(double(spam?: true))
end
it 'returns false' do
@@ -43,7 +43,7 @@ describe SpamService do
context 'when indicated as spam by akismet' do
before do
- allow(AkismetService).to receive(:new).and_return(double(is_spam?: true))
+ allow(AkismetService).to receive(:new).and_return(double(spam?: true))
end
it 'doesnt check as spam when request is missing' do
@@ -71,7 +71,7 @@ describe SpamService do
context 'when not indicated as spam by akismet' do
before do
- allow(AkismetService).to receive(:new).and_return(double(is_spam?: false))
+ allow(AkismetService).to receive(:new).and_return(double(spam?: false))
end
it 'returns false' do
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 8f1eb4863d9..c2d6d7781b9 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -3,9 +3,9 @@ require 'spec_helper'
describe SystemNoteService do
include Gitlab::Routing
- let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
- let(:author) { create(:user) }
+ set(:group) { create(:group) }
+ set(:project) { create(:project, :repository, group: group) }
+ set(:author) { create(:user) }
let(:noteable) { create(:issue, project: project) }
let(:issue) { noteable }
@@ -13,45 +13,23 @@ describe SystemNoteService do
let(:expected_noteable) { noteable }
let(:commit_count) { nil }
- it 'is valid' do
+ it 'has the correct attributes', :aggregate_failures do
expect(subject).to be_valid
- end
+ expect(subject).to be_system
- it 'sets the noteable model' do
expect(subject.noteable).to eq expected_noteable
- end
-
- it 'sets the project' do
expect(subject.project).to eq project
- end
-
- it 'sets the author' do
expect(subject.author).to eq author
- end
-
- it 'is a system note' do
- expect(subject).to be_system
- end
-
- context 'metadata' do
- it 'creates a new system note metadata record' do
- expect { subject }.to change{ SystemNoteMetadata.count }.from(0).to(1)
- end
- it 'creates a record correctly' do
- metadata = subject.system_note_metadata
-
- expect(metadata.commit_count).to eq(commit_count)
- expect(metadata.action).to eq(action)
- end
+ expect(subject.system_note_metadata.action).to eq(action)
+ expect(subject.system_note_metadata.commit_count).to eq(commit_count)
end
end
describe '.add_commits' do
subject { described_class.add_commits(noteable, project, author, new_commits, old_commits, oldrev) }
- let(:project) { create(:project, :repository) }
- let(:noteable) { create(:merge_request, source_project: project) }
+ let(:noteable) { create(:merge_request, source_project: project, target_project: project) }
let(:new_commits) { noteable.commits }
let(:old_commits) { [] }
let(:oldrev) { nil }
@@ -206,7 +184,7 @@ describe SystemNoteService do
describe '.change_label' do
subject { described_class.change_label(noteable, project, author, added, removed) }
- let(:labels) { create_list(:label, 2) }
+ let(:labels) { create_list(:label, 2, project: project) }
let(:added) { [] }
let(:removed) { [] }
@@ -315,7 +293,6 @@ describe SystemNoteService do
end
describe '.merge_when_pipeline_succeeds' do
- let(:project) { create(:project, :repository) }
let(:pipeline) { build(:ci_pipeline_without_jobs )}
let(:noteable) do
create(:merge_request, source_project: project, target_project: project)
@@ -333,7 +310,6 @@ describe SystemNoteService do
end
describe '.cancel_merge_when_pipeline_succeeds' do
- let(:project) { create(:project, :repository) }
let(:noteable) do
create(:merge_request, source_project: project, target_project: project)
end
@@ -411,7 +387,6 @@ describe SystemNoteService do
describe '.change_branch' do
subject { described_class.change_branch(noteable, project, author, 'target', old_branch, new_branch) }
- let(:project) { create(:project, :repository) }
let(:old_branch) { 'old_branch'}
let(:new_branch) { 'new_branch'}
@@ -429,8 +404,6 @@ describe SystemNoteService do
describe '.change_branch_presence' do
subject { described_class.change_branch_presence(noteable, project, author, :source, 'feature', :delete) }
- let(:project) { create(:project, :repository) }
-
it_behaves_like 'a system note' do
let(:action) { 'branch' }
end
@@ -445,8 +418,6 @@ describe SystemNoteService do
describe '.new_issue_branch' do
subject { described_class.new_issue_branch(noteable, project, author, "1-mepmep") }
- let(:project) { create(:project, :repository) }
-
it_behaves_like 'a system note' do
let(:action) { 'branch' }
end
@@ -477,7 +448,7 @@ describe SystemNoteService do
end
it 'does not create a system note metadata record' do
- expect { subject }.not_to change{ SystemNoteMetadata.count }
+ expect { subject }.not_to change { SystemNoteMetadata.count }
end
end
@@ -492,7 +463,7 @@ describe SystemNoteService do
describe 'note_body' do
context 'cross-project' do
- let(:project2) { create(:project, :repository) }
+ let(:project2) { create(:project, :repository) }
let(:mentioner) { create(:issue, project: project2) }
context 'from Commit' do
@@ -512,7 +483,6 @@ describe SystemNoteService do
context 'within the same project' do
context 'from Commit' do
- let(:project) { create(:project, :repository) }
let(:mentioner) { project.repository.commit }
it 'references the mentioning commit' do
@@ -554,7 +524,6 @@ describe SystemNoteService do
end
context 'when mentioner is a MergeRequest' do
- let(:project) { create(:project, :repository) }
let(:mentioner) { create(:merge_request, :simple, source_project: project) }
let(:noteable) { project.commit }
@@ -582,7 +551,6 @@ describe SystemNoteService do
end
describe '.cross_reference_exists?' do
- let(:project) { create(:project, :repository) }
let(:commit0) { project.commit }
let(:commit1) { project.commit('HEAD~2') }
@@ -920,9 +888,8 @@ describe SystemNoteService do
end
describe '.discussion_continued_in_issue' do
- let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
+ let(:discussion) { create(:diff_note_on_merge_request, project: project).to_discussion }
let(:merge_request) { discussion.noteable }
- let(:project) { merge_request.source_project }
let(:issue) { create(:issue, project: project) }
def reloaded_merge_request
@@ -1044,7 +1011,6 @@ describe SystemNoteService do
end
describe '.add_merge_request_wip_from_commit' do
- let(:project) { create(:project, :repository) }
let(:noteable) do
create(:merge_request, source_project: project, target_project: project)
end
@@ -1099,9 +1065,8 @@ describe SystemNoteService do
end
describe '.diff_discussion_outdated' do
- let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
+ let(:discussion) { create(:diff_note_on_merge_request, project: project).to_discussion }
let(:merge_request) { discussion.noteable }
- let(:project) { merge_request.source_project }
let(:change_position) { discussion.position }
def reloaded_merge_request
diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb
index 1b31ce29f7a..57013b54560 100644
--- a/spec/services/tags/create_service_spec.rb
+++ b/spec/services/tags/create_service_spec.rb
@@ -41,7 +41,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(GitHooksService::PreReceiveError, 'something went wrong')
+ .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'something went wrong')
response = service.execute('v1.1.0', 'master', 'Foo')
diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb
index 4218c15a3ce..28dfa9cf59c 100644
--- a/spec/services/test_hooks/project_service_spec.rb
+++ b/spec/services/test_hooks/project_service_spec.rb
@@ -21,6 +21,7 @@ describe TestHooks::ProjectService do
context 'push_events' do
let(:trigger) { 'push_events' }
+ let(:trigger_key) { :push_hooks }
it 'returns error message if not enough data' do
allow(project).to receive(:empty_repo?).and_return(true)
@@ -33,13 +34,14 @@ describe TestHooks::ProjectService do
allow(project).to receive(:empty_repo?).and_return(false)
allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'tag_push_events' do
let(:trigger) { 'tag_push_events' }
+ let(:trigger_key) { :tag_push_hooks }
it 'returns error message if not enough data' do
allow(project).to receive(:empty_repo?).and_return(true)
@@ -52,13 +54,14 @@ describe TestHooks::ProjectService do
allow(project).to receive(:empty_repo?).and_return(false)
allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'note_events' do
let(:trigger) { 'note_events' }
+ let(:trigger_key) { :note_hooks }
it 'returns error message if not enough data' do
expect(hook).not_to receive(:execute)
@@ -69,13 +72,14 @@ describe TestHooks::ProjectService do
allow(project).to receive(:notes).and_return([Note.new])
allow(Gitlab::DataBuilder::Note).to receive(:build).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'issues_events' do
let(:trigger) { 'issues_events' }
+ let(:trigger_key) { :issue_hooks }
let(:issue) { build(:issue) }
it 'returns error message if not enough data' do
@@ -87,13 +91,14 @@ describe TestHooks::ProjectService do
allow(project).to receive(:issues).and_return([issue])
allow(issue).to receive(:to_hook_data).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'confidential_issues_events' do
let(:trigger) { 'confidential_issues_events' }
+ let(:trigger_key) { :confidential_issue_hooks }
let(:issue) { build(:issue) }
it 'returns error message if not enough data' do
@@ -105,13 +110,14 @@ describe TestHooks::ProjectService do
allow(project).to receive(:issues).and_return([issue])
allow(issue).to receive(:to_hook_data).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'merge_requests_events' do
let(:trigger) { 'merge_requests_events' }
+ let(:trigger_key) { :merge_request_hooks }
it 'returns error message if not enough data' do
expect(hook).not_to receive(:execute)
@@ -122,13 +128,14 @@ describe TestHooks::ProjectService do
create(:merge_request, source_project: project)
allow_any_instance_of(MergeRequest).to receive(:to_hook_data).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'job_events' do
let(:trigger) { 'job_events' }
+ let(:trigger_key) { :job_hooks }
it 'returns error message if not enough data' do
expect(hook).not_to receive(:execute)
@@ -139,13 +146,14 @@ describe TestHooks::ProjectService do
create(:ci_build, project: project)
allow(Gitlab::DataBuilder::Build).to receive(:build).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'pipeline_events' do
let(:trigger) { 'pipeline_events' }
+ let(:trigger_key) { :pipeline_hooks }
it 'returns error message if not enough data' do
expect(hook).not_to receive(:execute)
@@ -156,13 +164,14 @@ describe TestHooks::ProjectService do
create(:ci_empty_pipeline, project: project)
allow(Gitlab::DataBuilder::Pipeline).to receive(:build).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'wiki_page_events' do
let(:trigger) { 'wiki_page_events' }
+ let(:trigger_key) { :wiki_page_hooks }
it 'returns error message if wiki disabled' do
allow(project).to receive(:wiki_enabled?).and_return(false)
@@ -180,7 +189,7 @@ describe TestHooks::ProjectService do
create(:wiki_page, wiki: project.wiki)
allow(Gitlab::DataBuilder::WikiPage).to receive(:build).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb
index 00d89924766..ff8b9595538 100644
--- a/spec/services/test_hooks/system_service_spec.rb
+++ b/spec/services/test_hooks/system_service_spec.rb
@@ -7,7 +7,6 @@ describe TestHooks::SystemService do
let(:project) { create(:project, :repository) }
let(:hook) { create(:system_hook) }
let(:service) { described_class.new(hook, current_user, trigger) }
- let(:sample_data) { { data: 'sample' }}
let(:success_result) { { status: :success, http_status: 200, message: 'ok' } }
before do
@@ -25,56 +24,39 @@ describe TestHooks::SystemService do
context 'push_events' do
let(:trigger) { 'push_events' }
-
- it 'returns error message if not enough data' do
- allow(project).to receive(:empty_repo?).and_return(true)
-
- expect(hook).not_to receive(:execute)
- expect(service.execute).to include({ status: :error, message: "Ensure project \"#{project.human_name}\" has commits." })
- end
+ let(:trigger_key) { :push_hooks }
it 'executes hook' do
allow(project).to receive(:empty_repo?).and_return(false)
- allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data)
+ expect(Gitlab::DataBuilder::Push).to receive(:sample_data).and_call_original
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'tag_push_events' do
let(:trigger) { 'tag_push_events' }
-
- it 'returns error message if not enough data' do
- allow(project.repository).to receive(:tags).and_return([])
-
- expect(hook).not_to receive(:execute)
- expect(service.execute).to include({ status: :error, message: "Ensure project \"#{project.human_name}\" has tags." })
- end
+ let(:trigger_key) { :tag_push_hooks }
it 'executes hook' do
allow(project.repository).to receive(:tags).and_return(['tag'])
- allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data)
+ expect(Gitlab::DataBuilder::Push).to receive(:sample_data).and_call_original
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'repository_update_events' do
let(:trigger) { 'repository_update_events' }
-
- it 'returns error message if not enough data' do
- allow(project).to receive(:commit).and_return(nil)
- expect(hook).not_to receive(:execute)
- expect(service.execute).to include({ status: :error, message: "Ensure project \"#{project.human_name}\" has commits." })
- end
+ let(:trigger_key) { :repository_update_hooks }
it 'executes hook' do
allow(project).to receive(:empty_repo?).and_return(false)
- allow(Gitlab::DataBuilder::Repository).to receive(:update).and_return(sample_data)
+ expect(Gitlab::DataBuilder::Repository).to receive(:sample_data).and_call_original
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Repository::SAMPLE_DATA, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 80d05451e09..a9b34a5258a 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -586,13 +586,13 @@ describe TodoService do
it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is pending' do
create(:todo, :directly_addressed, user: member, project: project, target: addressed_mr_assigned, author: author)
- expect{ service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count)
+ expect { service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count)
end
it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is done' do
create(:todo, :directly_addressed, user: skipped, project: project, target: addressed_mr_assigned, author: author)
- expect{ service.update_merge_request(addressed_mr_assigned, author, skip_users) }.not_to change(skipped.todos, :count)
+ expect { service.update_merge_request(addressed_mr_assigned, author, skip_users) }.not_to change(skipped.todos, :count)
end
context 'with a task list' do
diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb
index 14a5e40350a..87a90378e2b 100644
--- a/spec/services/user_project_access_changed_service_spec.rb
+++ b/spec/services/user_project_access_changed_service_spec.rb
@@ -8,5 +8,12 @@ describe UserProjectAccessChangedService do
described_class.new([1, 2]).execute
end
+
+ it 'permits non-blocking operation' do
+ expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
+ .with([[1], [2]])
+
+ described_class.new([1, 2]).execute(blocking: false)
+ end
end
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index a82567f6f43..58a5bede3de 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -4,9 +4,10 @@ describe Users::DestroyService do
describe "Deletes a user and all their personal projects" do
let!(:user) { create(:user) }
let!(:admin) { create(:admin) }
- let!(:namespace) { create(:namespace, owner: user) }
+ let!(:namespace) { user.namespace }
let!(:project) { create(:project, namespace: namespace) }
let(:service) { described_class.new(admin) }
+ let(:gitlab_shell) { Gitlab::Shell.new }
context 'no options are given' do
it 'deletes the user' do
@@ -14,7 +15,7 @@ describe Users::DestroyService do
expect { user_data['email'].to eq(user.email) }
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
- expect { Namespace.with_deleted.find(user.namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { Namespace.with_deleted.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'will delete the project' do
@@ -165,5 +166,27 @@ describe Users::DestroyService do
expect(Issue.exists?(issue.id)).to be_falsy
end
end
+
+ describe "user personal's repository removal" do
+ before do
+ Sidekiq::Testing.inline! { service.execute(user) }
+ end
+
+ context 'legacy storage' do
+ let!(:project) { create(:project, :empty_repo, namespace: user.namespace) }
+
+ it 'removes repository' do
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey
+ end
+ end
+
+ context 'hashed storage' do
+ let!(:project) { create(:project, :empty_repo, :hashed, namespace: user.namespace) }
+
+ it 'removes repository' do
+ expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_falsey
+ end
+ end
+ end
end
end
diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb
index 343804e3de0..6ee35a33b2d 100644
--- a/spec/services/users/update_service_spec.rb
+++ b/spec/services/users/update_service_spec.rb
@@ -12,9 +12,22 @@ describe Users::UpdateService do
end
it 'returns an error result when record cannot be updated' do
+ result = {}
expect do
- update_user(user, { email: 'invalid' })
+ result = update_user(user, { email: 'invalid' })
end.not_to change { user.reload.email }
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Email is invalid')
+ end
+
+ it 'includes namespace error messages' do
+ create(:group, name: 'taken', path: 'something_else')
+ result = {}
+ expect do
+ result = update_user(user, { username: 'taken' })
+ end.not_to change { user.reload.username }
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Namespace name has already been taken')
end
def update_user(user, opts)
@@ -24,7 +37,10 @@ describe Users::UpdateService do
describe '#execute!' do
it 'updates the name' do
- result = update_user(user, name: 'New Name')
+ service = described_class.new(user, name: 'New Name')
+ expect(service).not_to receive(:notify_new_user)
+
+ result = service.execute!
expect(result).to be true
expect(user.name).to eq('New Name')
@@ -36,6 +52,18 @@ describe Users::UpdateService do
end.to raise_error(ActiveRecord::RecordInvalid)
end
+ 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')
+ expect(service).to receive(:notify_new_user).and_call_original
+ expect(service).to receive(:system_hook_service).and_return(system_hook_service)
+
+ service.execute
+
+ expect(system_hook_service).to have_received(:execute_hooks_for).with(user, :create)
+ end
+
def update_user(user, opts)
described_class.new(user, opts).execute!
end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 79d90defd78..a669429ce3e 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -12,10 +12,10 @@ describe WebHookService do
let(:data) do
{ before: 'oldrev', after: 'newrev', ref: 'ref' }
end
- let(:service_instance) { described_class.new(project_hook, data, 'push_hooks') }
+ let(:service_instance) { described_class.new(project_hook, data, :push_hooks) }
describe '#execute' do
- before(:each) do
+ before do
project.hooks << [project_hook]
WebMock.stub_request(:post, project_hook.url)
@@ -144,7 +144,7 @@ describe WebHookService do
describe '#async_execute' do
let(:system_hook) { create(:system_hook) }
-
+
it 'enqueue WebHookWorker' do
expect(Sidekiq::Client).to receive(:enqueue).with(WebHookWorker, project_hook.id, data, 'push_hooks')
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
index ac2c89b3ff9..25ddf932d42 100644
--- a/spec/simplecov_env.rb
+++ b/spec/simplecov_env.rb
@@ -36,18 +36,25 @@ module SimpleCovEnv
track_files '{app,lib}/**/*.rb'
add_filter '/vendor/ruby/'
+ add_filter 'app/controllers/sherlock/'
add_filter 'config/initializers/'
+ add_filter 'db/fixtures/'
+ add_filter 'lib/gitlab/sidekiq_middleware/'
+ add_filter 'lib/system_check/'
add_group 'Controllers', 'app/controllers'
- add_group 'Models', 'app/models'
- add_group 'Mailers', 'app/mailers'
- add_group 'Helpers', 'app/helpers'
- add_group 'Workers', %w(app/jobs app/workers)
- add_group 'Libraries', 'lib'
- add_group 'Services', 'app/services'
- add_group 'Finders', 'app/finders'
- add_group 'Uploaders', 'app/uploaders'
- add_group 'Validators', 'app/validators'
+ add_group 'Finders', 'app/finders'
+ add_group 'Helpers', 'app/helpers'
+ add_group 'Libraries', 'lib'
+ add_group 'Mailers', 'app/mailers'
+ add_group 'Models', 'app/models'
+ add_group 'Policies', 'app/policies'
+ add_group 'Presenters', 'app/presenters'
+ add_group 'Serializers', 'app/serializers'
+ add_group 'Services', 'app/services'
+ add_group 'Uploaders', 'app/uploaders'
+ add_group 'Validators', 'app/validators'
+ add_group 'Workers', %w(app/jobs app/workers)
merge_timeout 365.days
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 0ba6ed56314..ff1754fbe7e 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -8,6 +8,7 @@ require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'shoulda/matchers'
require 'rspec/retry'
+require 'rspec-parameterized'
rspec_profiling_is_configured =
ENV['RSPEC_PROFILING_POSTGRES_URL'].present? ||
@@ -69,7 +70,14 @@ RSpec.configure do |config|
config.raise_errors_for_deprecations!
+ 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)
+ end
+
config.before(:suite) do
+ Timecop.safe_mode = true
TestEnv.init
end
@@ -97,9 +105,15 @@ RSpec.configure do |config|
reset_delivered_emails!
end
- if ENV['CI']
- config.around(:each) do |ex|
- ex.run_with_retry retry: 2
+ # Stub the `ForkedStorageCheck.storage_available?` method unless
+ # `:broken_storage` metadata is defined
+ #
+ # This check can be slow and is unnecessary in a test environment where we
+ # know the storage is available, because we create it at runtime
+ config.before(:example) do |example|
+ unless example.metadata[:broken_storage]
+ allow(Gitlab::Git::Storage::ForkedStorageCheck)
+ .to receive(:storage_available?).and_return(true)
end
end
@@ -130,17 +144,12 @@ RSpec.configure do |config|
Sidekiq.redis(&:flushall)
end
- config.before(:example, :migration) do
- ActiveRecord::Migrator
- .migrate(migrations_paths, previous_migration.version)
-
- reset_column_in_migration_models
+ config.before(:each, :migration) do
+ schema_migrate_down!
end
- config.after(:example, :migration) do
- ActiveRecord::Migrator.migrate(migrations_paths)
-
- reset_column_in_migration_models
+ config.after(:context, :migration) do
+ schema_migrate_up!
end
config.around(:each, :nested_groups) do |example|
diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb
index bf769225012..4bb5113957e 100644
--- a/spec/support/api/milestones_shared_examples.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -50,7 +50,7 @@ shared_examples_for 'group and project milestones' do |route_definition|
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
- expect(json_response.map{ |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id])
+ expect(json_response.map { |m| m['id'] }).to match_array([closed_milestone.id, other_milestone.id])
end
it 'does not return any milestone if none found' do
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index ac0aaa524b7..01aca74274c 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -45,18 +45,4 @@ module ApiHelpers
oauth_access_token: oauth_access_token
)
end
-
- def ci_api(path, user = nil)
- "/ci/api/v1/#{path}" +
-
- # Normalize query string
- (path.index('?') ? '' : '?') +
-
- # Append private_token if given a User object
- if user.respond_to?(:private_token)
- "&private_token=#{user.private_token}"
- else
- ''
- end
- end
end
diff --git a/spec/support/background_migrations_matchers.rb b/spec/support/background_migrations_matchers.rb
new file mode 100644
index 00000000000..423c0e4cefc
--- /dev/null
+++ b/spec/support/background_migrations_matchers.rb
@@ -0,0 +1,13 @@
+RSpec::Matchers.define :be_scheduled_migration do |delay, *expected|
+ match do |migration|
+ BackgroundMigrationWorker.jobs.any? do |job|
+ job['args'] == [migration, expected] &&
+ job['at'].to_i == (delay.to_i + Time.now.to_i)
+ end
+ end
+
+ failure_message do |migration|
+ "Migration `#{migration}` with args `#{expected.inspect}` " \
+ 'not scheduled in expected time!'
+ end
+end
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
index 4eec3127464..b23d81a226a 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -140,9 +140,14 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
context "when a namespace with the provider user's username already exists" do
- let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
+ let!(:existing_namespace) { user.namespace }
context "when the namespace is owned by the GitLab user" do
+ before do
+ user.username = other_username
+ user.save
+ end
+
it "takes the existing namespace" do
expect(Gitlab::GithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider)
@@ -153,12 +158,9 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
context "when the namespace is not owned by the GitLab user" do
- before do
- existing_namespace.owner = create(:user)
- existing_namespace.save
- end
-
it "creates a project using user's namespace" do
+ create(:user, username: other_username)
+
expect(Gitlab::GithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
.and_return(double(execute: true))
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 30911e7fa86..934b4557ba2 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -78,7 +78,10 @@ module CycleAnalyticsHelpers
@dummy_pipeline ||=
Ci::Pipeline.new(
sha: project.repository.commit('master').sha,
- project: project)
+ ref: 'master',
+ source: :push,
+ project: project,
+ protected: false)
end
def new_dummy_job(environment)
@@ -91,7 +94,8 @@ module CycleAnalyticsHelpers
ref: 'master',
tag: false,
name: 'dummy',
- pipeline: dummy_pipeline)
+ pipeline: dummy_pipeline,
+ protected: false)
end
end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index 7f5769209bb..edaee03ea6c 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -4,23 +4,23 @@ RSpec.configure do |config|
end
config.append_after(:context) do
- DatabaseCleaner.clean_with(:truncation)
+ DatabaseCleaner.clean_with(:truncation, cache_tables: false)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
- config.before(:each, js: true) do
+ config.before(:each, :js) do
DatabaseCleaner.strategy = :truncation
end
- config.before(:each, truncate: true) do
+ config.before(:each, :truncate) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each, :migration) do
- DatabaseCleaner.strategy = :truncation
+ DatabaseCleaner.strategy = :truncation, { cache_tables: false }
end
config.before(:each) do
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index bb4542b1683..81cb94ab8c4 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -14,6 +14,8 @@ shared_examples 'discussion comments' do |resource_name|
find(submit_selector).click
+ wait_for_requests
+
find(comments_selector, match: :first)
new_comment = all(comments_selector).last
@@ -26,6 +28,7 @@ shared_examples 'discussion comments' do |resource_name|
find("#{form_selector} .note-textarea").send_keys('a')
find(close_selector).click
+ wait_for_requests
find(comments_selector, match: :first)
find("#{comments_selector}.system-note")
@@ -76,12 +79,22 @@ shared_examples 'discussion comments' do |resource_name|
it 'clicking the ul padding or divider should not change the text' do
find(menu_selector).trigger 'click'
- expect(page).to have_selector menu_selector
- expect(find(dropdown_selector)).to have_content 'Comment'
+ if resource_name == 'issue'
+ expect(find(dropdown_selector)).to have_content 'Comment'
+
+ find(toggle_selector).click
+ find("#{menu_selector} .divider").trigger 'click'
+ else
+ find(menu_selector).trigger 'click'
- find("#{menu_selector} .divider").trigger 'click'
+ expect(page).to have_selector menu_selector
+ expect(find(dropdown_selector)).to have_content 'Comment'
+
+ find("#{menu_selector} .divider").trigger 'click'
+
+ expect(page).to have_selector menu_selector
+ end
- expect(page).to have_selector menu_selector
expect(find(dropdown_selector)).to have_content 'Comment'
end
@@ -91,9 +104,8 @@ shared_examples 'discussion comments' do |resource_name|
all("#{menu_selector} li").last.click
end
- it 'updates the submit button text, note_type input and closes the dropdown' do
+ it 'updates the submit button text and closes the dropdown' do
expect(find(dropdown_selector)).to have_content 'Start discussion'
- expect(find("#{form_selector} #note_type", visible: false).value).to eq('DiscussionNote')
expect(page).not_to have_selector menu_selector
end
@@ -157,9 +169,8 @@ shared_examples 'discussion comments' do |resource_name|
find("#{menu_selector} li", match: :first).click
end
- it 'updates the submit button text, clears the note_type input and closes the dropdown' do
+ it 'updates the submit button text and closes the dropdown' do
expect(find(dropdown_selector)).to have_content 'Comment'
- expect(find("#{form_selector} #note_type", visible: false).value).to eq('')
expect(page).not_to have_selector menu_selector
end
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 68f0ce8afb3..8282ba7e536 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -21,7 +21,7 @@ shared_examples 'issuable record that supports quick actions in its description
before do
project.team << [master, :master]
- sign_in(master)
+ gitlab_sign_in(master)
end
after do
@@ -119,16 +119,15 @@ shared_examples 'issuable record that supports quick actions in its description
guest = create(:user)
project.add_guest(guest)
- sign_out(:user)
- sign_in(guest)
-
+ gitlab_sign_out
+ gitlab_sign_in(guest)
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
end
it "does not close the #{issuable_type}" do
write_note("/close")
- expect(page).not_to have_content '/close'
+ expect(page).to have_content '/close'
expect(page).not_to have_content 'Commands applied'
expect(issuable).to be_open
@@ -158,16 +157,15 @@ shared_examples 'issuable record that supports quick actions in its description
guest = create(:user)
project.add_guest(guest)
- sign_out(:user)
- sign_in(guest)
-
+ gitlab_sign_out
+ gitlab_sign_in(guest)
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
end
it "does not reopen the #{issuable_type}" do
write_note("/reopen")
- expect(page).not_to have_content '/reopen'
+ expect(page).to have_content '/reopen'
expect(page).not_to have_content 'Commands applied'
expect(issuable).to be_closed
@@ -192,15 +190,15 @@ shared_examples 'issuable record that supports quick actions in its description
guest = create(:user)
project.add_guest(guest)
- sign_out(:user)
- sign_in(guest)
+ gitlab_sign_out
+ gitlab_sign_in(guest)
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
end
it "does not reopen the #{issuable_type}" do
write_note("/title Awesome new title")
- expect(page).not_to have_content '/title'
+ expect(page).to have_content '/title'
expect(page).not_to have_content 'Commands applied'
expect(issuable.reload.title).not_to eq 'Awesome new title'
@@ -292,7 +290,7 @@ shared_examples 'issuable record that supports quick actions in its description
end
end
- describe "preview of note on #{issuable_type}" do
+ describe "preview of note on #{issuable_type}", js: true do
it 'removes quick actions from note and explains them' do
create(:user, username: 'bob')
diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb
index 27e079c01dd..192a2fed0a8 100644
--- a/spec/support/features/reportable_note_shared_examples.rb
+++ b/spec/support/features/reportable_note_shared_examples.rb
@@ -1,23 +1,31 @@
require 'spec_helper'
-shared_examples 'reportable note' do
+shared_examples 'reportable note' do |type|
include NotesHelper
let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") }
let(:more_actions_selector) { '.more-actions.dropdown' }
let(:abuse_report_path) { new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) }
+ it 'has an edit button' do
+ expect(comment).to have_selector('.js-note-edit')
+ end
+
it 'has a `More actions` dropdown' do
expect(comment).to have_selector(more_actions_selector)
end
- it 'dropdown has Edit, Report and Delete links' do
+ it 'dropdown has Report and Delete links' do
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- expect(dropdown).to have_button('Edit comment')
expect(dropdown).to have_link('Report as abuse', href: abuse_report_path)
- expect(dropdown).to have_link('Delete comment', href: note_url(note, project))
+
+ if type == 'issue'
+ expect(dropdown).to have_button('Delete comment')
+ else
+ expect(dropdown).to have_link('Delete comment', href: note_url(note, project))
+ end
end
it 'Report button links to a report page' do
@@ -31,7 +39,7 @@ shared_examples 'reportable note' do
end
def open_dropdown(dropdown)
- dropdown.click
+ dropdown.find('.more-actions-toggle').trigger('click')
dropdown.find('.dropdown-menu li', match: :first)
end
end
diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb
index d21c4324d9e..05021ea9054 100644
--- a/spec/support/filtered_search_helpers.rb
+++ b/spec/support/filtered_search_helpers.rb
@@ -54,19 +54,51 @@ module FilteredSearchHelpers
# Iterates through each visual token inside
# .tokens-container to make sure the correct names and values are rendered
def expect_tokens(tokens)
- page.find '.filtered-search-box .tokens-container' do
- page.all(:css, '.tokens-container li').each_with_index do |el, index|
+ page.within '.filtered-search-box .tokens-container' do
+ page.all(:css, '.tokens-container li .selectable').each_with_index do |el, index|
token_name = tokens[index][:name]
token_value = tokens[index][:value]
+ token_emoji = tokens[index][:emoji_name]
expect(el.find('.name')).to have_content(token_name)
if token_value
expect(el.find('.value')).to have_content(token_value)
end
+ # gl-emoji content is blank when the emoji unicode is not supported
+ if token_emoji
+ selector = %(gl-emoji[data-name="#{token_emoji}"])
+ expect(el.find('.value')).to have_css(selector)
+ end
end
end
end
+ def create_token(token_name, token_value = nil, symbol = nil)
+ { name: token_name, value: "#{symbol}#{token_value}" }
+ end
+
+ def author_token(author_name = nil)
+ create_token('Author', author_name)
+ end
+
+ def assignee_token(assignee_name = nil)
+ create_token('Assignee', assignee_name)
+ end
+
+ def milestone_token(milestone_name = nil, has_symbol = true)
+ symbol = has_symbol ? '%' : nil
+ create_token('Milestone', milestone_name, symbol)
+ end
+
+ def label_token(label_name = nil, has_symbol = true)
+ symbol = has_symbol ? '~' : nil
+ create_token('Label', label_name, symbol)
+ end
+
+ def emoji_token(emoji_name = nil)
+ { name: 'My-Reaction', emoji_name: emoji_name }
+ end
+
def default_placeholder
'Search or filter results...'
end
diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb
index c89389b90ca..ef3c8e7087f 100755
--- a/spec/support/generate-seed-repo-rb
+++ b/spec/support/generate-seed-repo-rb
@@ -1,16 +1,16 @@
#!/usr/bin/env ruby
-#
+#
# # generate-seed-repo-rb
-#
+#
# This script generates the seed_repo.rb file used by lib/gitlab/git
# tests. The seed_repo.rb file needs to be updated anytime there is a
# Git push to https://gitlab.com/gitlab-org/gitlab-git-test.
-#
+#
# Usage:
-#
+#
# ./spec/support/generate-seed-repo-rb > spec/support/seed_repo.rb
-#
-#
+#
+#
require 'erb'
require 'tempfile'
diff --git a/spec/support/gitlab-git-test.git/objects/3e/20715310a699808282e772720b9c04a0696bcc b/spec/support/gitlab-git-test.git/objects/3e/20715310a699808282e772720b9c04a0696bcc
new file mode 100644
index 00000000000..86bf37ac887
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/3e/20715310a699808282e772720b9c04a0696bcc
Binary files differ
diff --git a/spec/support/gitlab-git-test.git/objects/95/96bc54a6f0c0c98248fe97077eb5ccf48a98d0 b/spec/support/gitlab-git-test.git/objects/95/96bc54a6f0c0c98248fe97077eb5ccf48a98d0
new file mode 100644
index 00000000000..d90cb028e9b
--- /dev/null
+++ b/spec/support/gitlab-git-test.git/objects/95/96bc54a6f0c0c98248fe97077eb5ccf48a98d0
@@ -0,0 +1,2 @@
+x•OËn1 䜯ð 9&O ¡ž¹"õnoYD6Õ’ýÒª?Ðã¼ì™Òj;w»¾ªÂQ £õG‹rN˜(‘ÆH‘PrAÇrRÌ7¯ºtpâ²Mì#MœÂ”c´ê…NrsI­
+Ž“%Þú½­páÒ>Û«²®pzüÀ¯¹?Y¥Õ3XäBBÌ°GB4ƒ ûpÿ?k®›ÔvÛžú‚y~Wø])[‡¥Áa<þ“ÍCP_ \ No newline at end of file
diff --git a/spec/support/gitlab-git-test.git/packed-refs b/spec/support/gitlab-git-test.git/packed-refs
index ce5ab1f705b..507e4ce785a 100644
--- a/spec/support/gitlab-git-test.git/packed-refs
+++ b/spec/support/gitlab-git-test.git/packed-refs
@@ -8,6 +8,7 @@
46e1395e609395de004cacd4b142865ab0e52a29 refs/heads/gitattributes-updated
4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6 refs/heads/master
5937ac0a7beb003549fc5fd26fc247adbce4a52e refs/heads/merge-test
+9596bc54a6f0c0c98248fe97077eb5ccf48a98d0 refs/heads/missing-gitmodules
f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8 refs/tags/v1.0.0
^6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b refs/tags/v1.1.0
diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json
index cd55d63125e..688175369ae 100644
--- a/spec/support/gitlab_stubs/session.json
+++ b/spec/support/gitlab_stubs/session.json
@@ -7,7 +7,7 @@
"skype":"aertert",
"linkedin":"",
"twitter":"",
- "color_scheme_id":2,
+ "theme_id":2,"color_scheme_id":2,
"state":"active",
"created_at":"2012-12-21T13:02:20Z",
"extern_uid":null,
diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json
index cd55d63125e..ce8dfe5ae75 100644
--- a/spec/support/gitlab_stubs/user.json
+++ b/spec/support/gitlab_stubs/user.json
@@ -7,7 +7,7 @@
"skype":"aertert",
"linkedin":"",
"twitter":"",
- "color_scheme_id":2,
+ "theme_id":2,"color_scheme_id":2,
"state":"active",
"created_at":"2012-12-21T13:02:20Z",
"extern_uid":null,
@@ -17,4 +17,4 @@
"can_create_project":false,
"private_token":"Wvjy2Krpb7y8xi93owUz",
"access_token":"Wvjy2Krpb7y8xi93owUz"
-}
+} \ No newline at end of file
diff --git a/spec/support/gpg_helpers.rb b/spec/support/gpg_helpers.rb
index 96ea6f28b30..65b38626a51 100644
--- a/spec/support/gpg_helpers.rb
+++ b/spec/support/gpg_helpers.rb
@@ -1,4 +1,6 @@
module GpgHelpers
+ SIGNED_COMMIT_SHA = '8a852d50dda17cc8fd1408d2fd0c5b0f24c76ca4'.freeze
+
module User1
extend self
diff --git a/spec/support/group_members_shared_example.rb b/spec/support/group_members_shared_example.rb
new file mode 100644
index 00000000000..547c83c7955
--- /dev/null
+++ b/spec/support/group_members_shared_example.rb
@@ -0,0 +1,27 @@
+RSpec.shared_examples 'members and requesters associations' do
+ describe '#members_and_requesters' do
+ it 'includes members and requesters' do
+ member_and_requester_user_ids = namespace.members_and_requesters.pluck(:user_id)
+
+ expect(member_and_requester_user_ids).to include(requester.id, developer.id)
+ end
+ end
+
+ describe '#members' do
+ it 'includes members and exclude requesters' do
+ member_user_ids = namespace.members.pluck(:user_id)
+
+ expect(member_user_ids).to include(developer.id)
+ expect(member_user_ids).not_to include(requester.id)
+ end
+ end
+
+ describe '#requesters' do
+ it 'does not include requesters' do
+ requester_user_ids = namespace.requesters.pluck(:user_id)
+
+ expect(requester_user_ids).to include(requester.id)
+ expect(requester_user_ids).not_to include(developer.id)
+ end
+ end
+end
diff --git a/spec/support/helpers/note_interaction_helpers.rb b/spec/support/helpers/note_interaction_helpers.rb
index 551c759133c..86008698692 100644
--- a/spec/support/helpers/note_interaction_helpers.rb
+++ b/spec/support/helpers/note_interaction_helpers.rb
@@ -2,7 +2,7 @@ module NoteInteractionHelpers
def open_more_actions_dropdown(note)
note_element = find("#note_#{note.id}")
- note_element.find('.more-actions').click
+ note_element.find('.more-actions-toggle').trigger('click')
note_element.find('.more-actions .dropdown-menu li', match: :first)
end
end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index 2011408be93..562423afc2a 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -35,7 +35,8 @@ module ExportFileHelper
project: project,
commit_id: ci_pipeline.sha)
- create(:event, :created, target: milestone, project: project, author: user)
+ event = create(:event, :created, target: milestone, project: project, author: user, action: 5)
+ create(:push_event_payload, event: event)
create(:project_member, :master, user: user, project: project)
create(:ci_variable, project: project)
create(:ci_trigger, project: project)
diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb
index a60d3b0d22d..75982432ab4 100644
--- a/spec/support/issuables_list_metadata_shared_examples.rb
+++ b/spec/support/issuables_list_metadata_shared_examples.rb
@@ -2,12 +2,12 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
before do
@issuable_ids = []
- 2.times do |n|
+ %w[fix improve/awesome].each do |source_branch|
issuable =
if issuable_type == :issue
create(issuable_type, project: project)
else
- create(issuable_type, source_project: project, source_branch: "#{n}-feature")
+ create(issuable_type, source_project: project, source_branch: source_branch)
end
@issuable_ids << issuable.id
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb
index aace4b3adee..923c8080e6c 100644
--- a/spec/support/javascript_fixtures_helpers.rb
+++ b/spec/support/javascript_fixtures_helpers.rb
@@ -31,6 +31,10 @@ module JavaScriptFixturesHelpers
File.write(fixture_file_name, fixture)
end
+ def remove_repository(project)
+ Gitlab::Shell.new.remove_repository(project.repository_storage_path, project.disk_path)
+ end
+
private
# Private: Prepare a response object for use as a frontend fixture
diff --git a/spec/support/matchers/access_matchers_for_controller.rb b/spec/support/matchers/access_matchers_for_controller.rb
index ff60bd0c0ae..bb6b7c63ee9 100644
--- a/spec/support/matchers/access_matchers_for_controller.rb
+++ b/spec/support/matchers/access_matchers_for_controller.rb
@@ -1,6 +1,6 @@
# AccessMatchersForController
#
-# For testing authorize_xxx in controller.
+# For testing authorize_xxx in controller.
module AccessMatchersForController
extend RSpec::Matchers::DSL
include Warden::Test::Helpers
diff --git a/spec/support/matchers/query_matcher.rb b/spec/support/matchers/query_matcher.rb
index ac8c4ab91d9..bb0feca7c43 100644
--- a/spec/support/matchers/query_matcher.rb
+++ b/spec/support/matchers/query_matcher.rb
@@ -28,6 +28,6 @@ RSpec::Matchers.define :make_queries_matching do |matcher, expected_count = nil|
def query_count(regex, &block)
@recorder = ActiveRecord::QueryRecorder.new(&block).log
- @recorder.select{ |q| q.match(regex) }
+ @recorder.select { |q| q.match(regex) }
end
end
diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb
index aabdad13047..4ca019c1b05 100644
--- a/spec/support/migrations_helpers.rb
+++ b/spec/support/migrations_helpers.rb
@@ -16,6 +16,8 @@ module MigrationsHelpers
end
def reset_column_in_migration_models
+ ActiveRecord::Base.clear_cache!
+
described_class.constants.sort.each do |name|
const = described_class.const_get(name)
@@ -31,6 +33,35 @@ module MigrationsHelpers
end
end
+ def migration_schema_version
+ self.class.metadata[:schema] || previous_migration.version
+ end
+
+ def schema_migrate_down!
+ disable_migrations_output do
+ ActiveRecord::Migrator.migrate(migrations_paths,
+ migration_schema_version)
+ end
+
+ reset_column_in_migration_models
+ end
+
+ def schema_migrate_up!
+ disable_migrations_output do
+ ActiveRecord::Migrator.migrate(migrations_paths)
+ end
+
+ reset_column_in_migration_models
+ end
+
+ def disable_migrations_output
+ ActiveRecord::Migration.verbose = false
+
+ yield
+ ensure
+ ActiveRecord::Migration.verbose = true
+ end
+
def migrate!
ActiveRecord::Migrator.up(migrations_paths) do |migration|
migration.name == described_class.name
diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb
index 136f92c6419..e2c23607406 100644
--- a/spec/support/notify_shared_examples.rb
+++ b/spec/support/notify_shared_examples.rb
@@ -1,9 +1,10 @@
shared_context 'gitlab email notification' do
+ set(:project) { create(:project, :repository) }
+ set(:recipient) { create(:user, email: 'recipient@example.com') }
+
let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name }
let(:gitlab_sender) { Gitlab.config.gitlab.email_from }
let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to }
- let(:recipient) { create(:user, email: 'recipient@example.com') }
- let(:project) { create(:project) }
let(:new_user_address) { 'newguy@example.com' }
before do
diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb
index 5cb415111d2..86bfeed107c 100644
--- a/spec/support/rake_helpers.rb
+++ b/spec/support/rake_helpers.rb
@@ -5,11 +5,15 @@ module RakeHelpers
end
def stub_warn_user_is_not_gitlab
- allow_any_instance_of(Object).to receive(:warn_user_is_not_gitlab)
+ allow(main_object).to receive(:warn_user_is_not_gitlab)
end
def silence_output
- allow($stdout).to receive(:puts)
- allow($stdout).to receive(:print)
+ allow(main_object).to receive(:puts)
+ allow(main_object).to receive(:print)
+ end
+
+ def main_object
+ @main_object ||= TOPLEVEL_BINDING.eval('self')
end
end
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index f9552e41894..8676f895a83 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -3,12 +3,12 @@ RSpec.shared_examples "redis_shared_examples" do
let(:test_redis_url) { "redis://redishost:#{redis_port}"}
- before(:each) do
+ before do
stub_env(environment_config_file_name, Rails.root.join(config_file_name))
clear_raw_config
end
- after(:each) do
+ after do
clear_raw_config
end
diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb
index 8731847592b..11ef1fc477f 100644
--- a/spec/support/seed_helper.rb
+++ b/spec/support/seed_helper.rb
@@ -41,7 +41,7 @@ module SeedHelper
end
def create_mutable_seeds
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}),
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}),
chdir: SEED_STORAGE_PATH,
out: '/dev/null',
err: '/dev/null')
diff --git a/spec/support/seed_repo.rb b/spec/support/seed_repo.rb
index cfe7fc980a8..b4868e82cd7 100644
--- a/spec/support/seed_repo.rb
+++ b/spec/support/seed_repo.rb
@@ -97,6 +97,7 @@ module SeedRepo
gitattributes-updated
master
merge-test
+ missing-gitmodules
].freeze
TAGS = %w[
v1.0.0
diff --git a/spec/support/shared_examples/requests/api/status_shared_examples.rb b/spec/support/shared_examples/requests/api/status_shared_examples.rb
index 226277411d6..7d7f66adeab 100644
--- a/spec/support/shared_examples/requests/api/status_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/status_shared_examples.rb
@@ -40,3 +40,28 @@ shared_examples_for '404 response' do
end
end
end
+
+shared_examples_for '412 response' do
+ let(:params) { nil }
+ let(:success_status) { 204 }
+
+ context 'for a modified ressource' do
+ before do
+ delete request, params, { 'HTTP_IF_UNMODIFIED_SINCE' => '1990-01-12T00:00:48-0600' }
+ end
+
+ it 'returns 412' do
+ expect(response).to have_gitlab_http_status(412)
+ end
+ end
+
+ context 'for an unmodified ressource' do
+ before do
+ delete request, params, { 'HTTP_IF_UNMODIFIED_SINCE' => Time.now }
+ end
+
+ it 'returns accepted' do
+ expect(response).to have_gitlab_http_status(success_status)
+ end
+ end
+end
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index 516f8878679..45c10e78789 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -38,6 +38,20 @@ module StubConfiguration
allow(Gitlab.config.backup).to receive_messages(to_settings(messages))
end
+ def stub_storage_settings(messages)
+ # 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['failure_count_threshold'] ||= 10
+ storage_settings['failure_wait_time'] ||= 30
+ storage_settings['failure_reset_time'] ||= 1800
+ storage_settings['storage_timeout'] ||= 5
+ end
+
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(Settingslogic.new(messages))
+ end
+
private
# Modifies stubbed messages to also stub possible predicate versions
diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb
index b8928867174..19fbe572930 100644
--- a/spec/support/stub_env.rb
+++ b/spec/support/stub_env.rb
@@ -1,5 +1,7 @@
# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb
module StubENV
+ include Gitlab::CurrentSettings
+
def stub_env(key_or_hash, value = nil)
init_stub unless env_stubbed?
if key_or_hash.is_a? Hash
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index e059c9620ab..71b9deeabc3 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -5,7 +5,7 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
- 'signed-commits' => '5d4a1cb',
+ 'signed-commits' => '2d1096e',
'not-merged-branch' => 'b83d6e3',
'branch-merged' => '498214d',
'empty-branch' => '7efb185',
@@ -176,6 +176,24 @@ module TestEnv
spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s
@gitaly_pid = Bundler.with_original_env { IO.popen([spawn_script], &:read).to_i }
+ wait_gitaly
+ end
+
+ def wait_gitaly
+ sleep_time = 10
+ sleep_interval = 0.1
+ socket = Gitlab::GitalyClient.address('default').sub('unix:', '')
+
+ Integer(sleep_time / sleep_interval).times do
+ begin
+ Socket.unix(socket)
+ return
+ rescue
+ sleep sleep_interval
+ end
+ end
+
+ raise "could not connect to gitaly at #{socket.inspect} after #{sleep_time} seconds"
end
def stop_gitaly
@@ -250,6 +268,14 @@ module TestEnv
"#{forked_repo_path}_bare"
end
+ def with_empty_bare_repository(name = nil)
+ path = Rails.root.join('tmp/tests', name || 'empty-bare-repository').to_s
+
+ yield(Rugged::Repository.init_at(path, :bare))
+ ensure
+ FileUtils.rm_rf(path)
+ end
+
private
def factory_repo_path
diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb
index ff0b47899f5..2dfa5fbecea 100644
--- a/spec/support/unique_ip_check_shared_examples.rb
+++ b/spec/support/unique_ip_check_shared_examples.rb
@@ -1,6 +1,6 @@
shared_context 'unique ips sign in limit' do
include StubENV
- before(:each) do
+ before do
Gitlab::Redis::Cache.with(&:flushall)
Gitlab::Redis::Queues.with(&:flushall)
Gitlab::Redis::SharedState.with(&:flushall)
diff --git a/spec/tasks/config_lint_spec.rb b/spec/tasks/config_lint_spec.rb
index ed6c5b09663..83d54259dfa 100644
--- a/spec/tasks/config_lint_spec.rb
+++ b/spec/tasks/config_lint_spec.rb
@@ -2,19 +2,19 @@ require 'rake_helper'
Rake.application.rake_require 'tasks/config_lint'
describe ConfigLint do
- let(:files){ ['lib/support/fake.sh'] }
+ let(:files) { ['lib/support/fake.sh'] }
it 'errors out if any bash scripts have errors' do
- expect { described_class.run(files){ system('exit 1') } }.to raise_error(SystemExit)
+ expect { described_class.run(files) { system('exit 1') } }.to raise_error(SystemExit)
end
it 'passes if all scripts are fine' do
- expect { described_class.run(files){ system('exit 0') } }.not_to raise_error
+ expect { described_class.run(files) { system('exit 0') } }.not_to raise_error
end
end
describe 'config_lint rake task' do
- before(:each) do
+ before do
# Prevent `system` from actually being called
allow(Kernel).to receive(:system).and_return(true)
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index fae92451b46..0c8c8a2ab05 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -119,7 +119,7 @@ describe 'gitlab:app namespace rake task' do
let(:project) { create(:project, :repository) }
let(:user_backup_path) { "repositories/#{project.disk_path}" }
- before(:each) do
+ before do
@origin_cd = Dir.pwd
path = File.join(project.repository.path_to_repo, filename)
@@ -130,7 +130,7 @@ describe 'gitlab:app namespace rake task' do
create_backup
end
- after(:each) do
+ after do
ENV["SKIP"] = ""
FileUtils.rm(@backup_tar)
Dir.chdir(@origin_cd)
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index cc932a4ec4c..1e9b20435ec 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -20,7 +20,7 @@ describe 'gitlab:gitaly namespace rake task' do
context 'when an underlying Git command fail' do
it 'aborts and display a help message' do
- expect_any_instance_of(Object)
+ expect(main_object)
.to receive(:checkout_or_clone_version).and_raise 'Git error'
expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error 'Git error'
@@ -33,7 +33,7 @@ describe 'gitlab:gitaly namespace rake task' do
end
it 'calls checkout_or_clone_version with the right arguments' do
- expect_any_instance_of(Object)
+ expect(main_object)
.to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path)
run_rake_task('gitlab:gitaly:install', clone_path)
@@ -41,7 +41,7 @@ describe 'gitlab:gitaly namespace rake task' do
end
describe 'gmake/make' do
- let(:command_preamble) { %w[/usr/bin/env -u RUBYOPT] }
+ let(:command_preamble) { %w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] }
before(:all) do
@old_env_ci = ENV.delete('CI')
@@ -54,17 +54,17 @@ describe 'gitlab:gitaly namespace rake task' do
before do
FileUtils.mkdir_p(clone_path)
expect(Dir).to receive(:chdir).with(clone_path).and_call_original
+ allow(Bundler).to receive(:bundle_path).and_return('/fake/bundle_path')
end
context 'gmake is available' do
before do
- expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
- allow_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['gmake']).and_return(true)
+ expect(main_object).to receive(:checkout_or_clone_version)
end
it 'calls gmake in the gitaly directory' do
expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0])
- expect_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['gmake']).and_return(true)
+ expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake BUNDLE_PATH=/fake/bundle_path]).and_return(true)
run_rake_task('gitlab:gitaly:install', clone_path)
end
@@ -72,16 +72,27 @@ describe 'gitlab:gitaly namespace rake task' do
context 'gmake is not available' do
before do
- expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
- allow_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true)
+ expect(main_object).to receive(:checkout_or_clone_version)
+ expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42])
end
it 'calls make in the gitaly directory' do
- expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42])
- expect_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true)
+ expect(main_object).to receive(:run_command!).with(command_preamble + %w[make BUNDLE_PATH=/fake/bundle_path]).and_return(true)
run_rake_task('gitlab:gitaly:install', clone_path)
end
+
+ context 'when Rails.env is not "test"' do
+ before do
+ allow(Rails.env).to receive(:test?).and_return(false)
+ end
+
+ it 'calls make in the gitaly directory without BUNDLE_PATH' do
+ expect(main_object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true)
+
+ run_rake_task('gitlab:gitaly:install', clone_path)
+ end
+ end
end
end
end
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index ee3614c50f6..65155cb044d 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -22,7 +22,8 @@ describe 'gitlab:shell rake tasks' do
describe 'create_hooks task' do
it 'calls gitlab-shell bin/create_hooks' do
expect_any_instance_of(Object).to receive(:system)
- .with("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks", *repository_storage_paths_args)
+ .with("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks",
+ *Gitlab::TaskHelpers.repository_storage_paths_args)
run_rake_task('gitlab:shell:create_hooks')
end
diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb
index 1b68f3044a4..42516d36c67 100644
--- a/spec/tasks/gitlab/workhorse_rake_spec.rb
+++ b/spec/tasks/gitlab/workhorse_rake_spec.rb
@@ -20,7 +20,7 @@ describe 'gitlab:workhorse namespace rake task' do
context 'when an underlying Git command fail' do
it 'aborts and display a help message' do
- expect_any_instance_of(Object)
+ expect(main_object)
.to receive(:checkout_or_clone_version).and_raise 'Git error'
expect { run_rake_task('gitlab:workhorse:install', clone_path) }.to raise_error 'Git error'
@@ -33,7 +33,7 @@ describe 'gitlab:workhorse namespace rake task' do
end
it 'calls checkout_or_clone_version with the right arguments' do
- expect_any_instance_of(Object)
+ expect(main_object)
.to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path)
run_rake_task('gitlab:workhorse:install', clone_path)
@@ -48,13 +48,13 @@ describe 'gitlab:workhorse namespace rake task' do
context 'gmake is available' do
before do
- expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
- allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true)
+ expect(main_object).to receive(:checkout_or_clone_version)
+ allow(Object).to receive(:run_command!).with(['gmake']).and_return(true)
end
it 'calls gmake in the gitlab-workhorse directory' do
expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0])
- expect_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true)
+ expect(main_object).to receive(:run_command!).with(['gmake']).and_return(true)
run_rake_task('gitlab:workhorse:install', clone_path)
end
@@ -62,13 +62,13 @@ describe 'gitlab:workhorse namespace rake task' do
context 'gmake is not available' do
before do
- expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
- allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true)
+ expect(main_object).to receive(:checkout_or_clone_version)
+ allow(main_object).to receive(:run_command!).with(['make']).and_return(true)
end
it 'calls make in the gitlab-workhorse directory' do
expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42])
- expect_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true)
+ expect(main_object).to receive(:run_command!).with(['make']).and_return(true)
run_rake_task('gitlab:workhorse:install', clone_path)
end
diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb
index d7c1b390f9a..0cf462e9553 100644
--- a/spec/uploaders/file_mover_spec.rb
+++ b/spec/uploaders/file_mover_spec.rb
@@ -4,11 +4,11 @@ describe FileMover do
let(:filename) { 'banana_sample.gif' }
let(:file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) }
let(:temp_description) do
- 'test ![banana_sample](/uploads/system/temp/secret55/banana_sample.gif) same ![banana_sample]'\
- '(/uploads/system/temp/secret55/banana_sample.gif)'
+ 'test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) same ![banana_sample]'\
+ '(/uploads/-/system/temp/secret55/banana_sample.gif)'
end
let(:temp_file_path) { File.join('secret55', filename).to_s }
- let(:file_path) { File.join('uploads', 'system', 'personal_snippet', snippet.id.to_s, 'secret55', filename).to_s }
+ let(:file_path) { File.join('uploads', '-', 'system', 'personal_snippet', snippet.id.to_s, 'secret55', filename).to_s }
let(:snippet) { create(:personal_snippet, description: temp_description) }
@@ -28,8 +28,8 @@ describe FileMover do
expect(snippet.reload.description)
.to eq(
- "test ![banana_sample](/uploads/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"\
- " same ![banana_sample](/uploads/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"
+ "test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"\
+ " same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"
)
end
@@ -50,8 +50,8 @@ describe FileMover do
expect(snippet.reload.description)
.to eq(
- "test ![banana_sample](/uploads/system/temp/secret55/banana_sample.gif)"\
- " same ![banana_sample](/uploads/system/temp/secret55/banana_sample.gif)"
+ "test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif)"\
+ " same ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif)"
)
end
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index e505edc75ce..cbafa9f478d 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -10,7 +10,7 @@ describe PersonalFileUploader do
dynamic_segment = "personal_snippet/#{snippet.id}"
- expect(described_class.absolute_path(upload)).to end_with("/system/#{dynamic_segment}/secret/foo.jpg")
+ expect(described_class.absolute_path(upload)).to end_with("/-/system/#{dynamic_segment}/secret/foo.jpg")
end
end
@@ -19,7 +19,7 @@ describe PersonalFileUploader do
uploader = described_class.new(snippet, 'secret')
allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name'))
- expected_url = "/uploads/system/personal_snippet/#{snippet.id}/secret/file_name"
+ expected_url = "/uploads/-/system/personal_snippet/#{snippet.id}/secret/file_name"
expect(uploader.to_h).to eq(
alt: 'file_name',
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index df742bf6848..b4359d819a0 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -9,6 +9,7 @@ describe 'admin/dashboard/index.html.haml' do
assign(:groups, create_list(:group, 1))
allow(view).to receive(:admin?).and_return(true)
+ allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
end
it "shows version of GitLab Workhorse" do
diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb
index 3390ae247ff..f2c19c7642a 100644
--- a/spec/views/ci/lints/show.html.haml_spec.rb
+++ b/spec/views/ci/lints/show.html.haml_spec.rb
@@ -73,8 +73,8 @@ describe 'ci/lints/show' do
render
expect(rendered).to have_content('Tag list: dotnet')
- expect(rendered).to have_content('Refs only: test@dude/repo')
- expect(rendered).to have_content('Refs except: deploy')
+ expect(rendered).to have_content('Only policy: refs, test@dude/repo')
+ expect(rendered).to have_content('Except policy: refs, deploy')
expect(rendered).to have_content('Environment: testing')
expect(rendered).to have_content('When: on_success')
end
diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
index 9adbb0476be..0870b8f09f9 100644
--- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -5,6 +5,7 @@ describe 'devise/shared/_signin_box' do
before do
stub_devise
assign(:ldap_servers, [])
+ allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
end
it 'is shown when Crowd is enabled' do
diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb
new file mode 100644
index 00000000000..38cfb84f0d5
--- /dev/null
+++ b/spec/views/groups/edit.html.haml_spec.rb
@@ -0,0 +1,116 @@
+require 'spec_helper'
+
+describe 'groups/edit.html.haml' do
+ include Devise::Test::ControllerHelpers
+
+ describe '"Share with group lock" setting' do
+ let(:root_owner) { create(:user) }
+ let(:root_group) { create(:group) }
+
+ before do
+ root_group.add_owner(root_owner)
+ end
+
+ shared_examples_for '"Share with group lock" setting' do |checkbox_options|
+ it 'should have the correct label, help text, and checkbox options' do
+ assign(:group, test_group)
+ allow(view).to receive(:can?).with(test_user, :admin_group, test_group).and_return(true)
+ allow(view).to receive(:can_change_group_visibility_level?).and_return(false)
+ allow(view).to receive(:current_user).and_return(test_user)
+ expect(view).to receive(:can_change_share_with_group_lock?).and_return(!checkbox_options[:disabled])
+ expect(view).to receive(:share_with_group_lock_help_text).and_return('help text here')
+
+ render
+
+ expect(rendered).to have_content("Prevent sharing a project within #{test_group.name} with other groups")
+ expect(rendered).to have_css('.descr', text: 'help text here')
+ expect(rendered).to have_field('group_share_with_group_lock', checkbox_options)
+ end
+ end
+
+ context 'for a root group' do
+ let(:test_group) { root_group }
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+
+ context 'for a subgroup', :nested_groups do
+ let!(:subgroup) { create(:group, parent: root_group) }
+ let(:sub_owner) { create(:user) }
+ let(:test_group) { subgroup }
+
+ context 'when the root_group has "Share with group lock" disabled' do
+ context 'when the subgroup has "Share with group lock" disabled' do
+ context 'as the root_owner' do
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+
+ context 'as the sub_owner' do
+ let(:test_user) { sub_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+ end
+
+ context 'when the subgroup has "Share with group lock" enabled' do
+ before do
+ subgroup.update_column(:share_with_group_lock, true)
+ end
+
+ context 'as the root_owner' do
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
+ end
+
+ context 'as the sub_owner' do
+ let(:test_user) { sub_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
+ end
+ end
+ end
+
+ context 'when the root_group has "Share with group lock" enabled' do
+ before do
+ root_group.update_column(:share_with_group_lock, true)
+ end
+
+ context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do
+ context 'as the root_owner' do
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+
+ context 'as the sub_owner' do
+ let(:test_user) { sub_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+ end
+
+ context 'when the subgroup has "Share with group lock" enabled (same as parent)' do
+ before do
+ subgroup.update_column(:share_with_group_lock, true)
+ end
+
+ context 'as the root_owner' do
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
+ end
+
+ context 'as the sub_owner' do
+ let(:test_user) { sub_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: true, checked: true }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb
index 1f8261cc46b..c030129559e 100644
--- a/spec/views/help/index.html.haml_spec.rb
+++ b/spec/views/help/index.html.haml_spec.rb
@@ -37,5 +37,6 @@ describe 'help/index' do
def stub_helpers
allow(view).to receive(:markdown).and_return('')
allow(view).to receive(:version_status_badge).and_return('')
+ allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
end
end
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index 8020faa1f9c..e8e6d2e7a75 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -1,6 +1,10 @@
require 'spec_helper'
describe 'layouts/_head' do
+ before do
+ allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
+ end
+
it 'escapes HTML-safe strings in page_title' do
stub_helper_with_safe_string(:page_title)
diff --git a/spec/views/layouts/nav/_project.html.haml_spec.rb b/spec/views/layouts/nav/_project.html.haml_spec.rb
deleted file mode 100644
index faea2505e40..00000000000
--- a/spec/views/layouts/nav/_project.html.haml_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'spec_helper'
-
-describe 'layouts/nav/_project' do
- describe 'container registry tab' do
- before do
- stub_container_registry_config(enabled: true)
-
- assign(:project, create(:project, :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)
- .and_return('projects/registry/repositories')
- end
-
- it 'has both Registry and Repository tabs' do
- render
-
- expect(rendered).to have_text 'Repository'
- expect(rendered).to have_text 'Registry'
- end
-
- it 'highlights only one tab' do
- render
-
- expect(rendered).to have_css('.active', count: 1)
- end
-
- it 'highlights container registry tab only' do
- render
-
- expect(rendered).to have_css('.active', text: 'Registry')
- end
- end
-end
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
new file mode 100644
index 00000000000..b17bc6692f3
--- /dev/null
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe 'layouts/nav/sidebar/_project' do
+ 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)
+ .and_return('projects/registry/repositories')
+ end
+
+ it 'has both Registry and Repository tabs' do
+ render
+
+ expect(rendered).to have_text 'Repository'
+ expect(rendered).to have_text 'Registry'
+ end
+
+ it 'highlights only one tab' do
+ render
+
+ expect(rendered).to have_css('.active', count: 1)
+ end
+
+ it 'highlights container registry tab only' do
+ render
+
+ expect(rendered).to have_css('.active', text: 'Registry')
+ end
+ end
+end
diff --git a/spec/views/projects/commits/_commit.html.haml_spec.rb b/spec/views/projects/commits/_commit.html.haml_spec.rb
new file mode 100644
index 00000000000..00547e433c4
--- /dev/null
+++ b/spec/views/projects/commits/_commit.html.haml_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe 'projects/commits/_commit.html.haml' do
+ before do
+ allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
+ end
+
+ context 'with a singed commit' do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:ref) { GpgHelpers::SIGNED_COMMIT_SHA }
+ let(:commit) { repository.commit(ref) }
+
+ it 'does not display a loading spinner for GPG status' do
+ render partial: 'projects/commits/commit', locals: {
+ project: project,
+ ref: ref,
+ commit: commit
+ }
+
+ within '.gpg-status-box' do
+ expect(page).not_to have_css('i.fa.fa-spinner.fa-spin')
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index 94899e26292..5c6b2e4b042 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -10,15 +10,18 @@ describe 'projects/edit' do
assign(:project, project)
allow(controller).to receive(:current_user).and_return(user)
- allow(view).to receive_messages(current_user: user, can?: true)
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ allow(view).to receive_messages(current_user: user,
+ can?: true,
+ current_application_settings: Gitlab::CurrentSettings.current_application_settings)
end
- context 'LFS enabled setting' do
- it 'displays the correct elements' do
+ context 'project export disabled' do
+ it 'does not display the project export option' do
+ stub_application_setting(project_export_enabled?: false)
+
render
- expect(rendered).to have_select('project_lfs_enabled')
- expect(rendered).to have_content('Git Large File Storage')
+
+ expect(rendered).not_to have_content('Export project')
end
end
end
diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb
index 117f48450e2..d4279626e75 100644
--- a/spec/views/projects/jobs/show.html.haml_spec.rb
+++ b/spec/views/projects/jobs/show.html.haml_spec.rb
@@ -195,20 +195,4 @@ describe 'projects/jobs/show' do
text: /\A\n#{Regexp.escape(commit_title)}\n\Z/)
end
end
-
- describe 'shows trigger variables in sidebar' do
- let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline) }
-
- before do
- build.trigger_request = trigger_request
- render
- end
-
- it 'shows trigger variables in separate lines' do
- expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_1')
- expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_2')
- expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_1')
- expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2')
- end
- end
end
diff --git a/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb
index 5770cf92b4e..9ab105c3238 100644
--- a/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/creations/_new_submit.html.haml_spec.rb
@@ -14,6 +14,7 @@ describe 'projects/merge_requests/creations/_new_submit.html.haml' do
allow(view).to receive(:can?).and_return(true)
allow(view).to receive(:url_for).and_return('#')
allow(view).to receive(:current_user).and_return(merge_request.author)
+ allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
end
context 'when there are pipelines for merge request but no pipeline for last commit' do
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 dc2fcc3e715..6f29d12373a 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -25,7 +25,9 @@ describe 'projects/merge_requests/show.html.haml' do
assign(:notes, [])
assign(:pipelines, Ci::Pipeline.none)
- allow(view).to receive_messages(current_user: user, can?: true)
+ allow(view).to receive_messages(current_user: user,
+ can?: true,
+ current_application_settings: Gitlab::CurrentSettings.current_application_settings)
end
context 'when the merge request is closed' do
diff --git a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
index aea20d826d0..9c0be249a50 100644
--- a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
+++ b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
@@ -24,18 +24,16 @@ describe 'projects/notes/_more_actions_dropdown' do
expect(rendered).not_to have_selector('.dropdown.more-actions')
end
- it 'shows Report as abuse, Edit and Delete buttons if editable and not current users comment' do
+ it 'shows Report as abuse and Delete buttons if editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: true, note: note
expect(rendered).to have_link('Report as abuse')
- expect(rendered).to have_button('Edit comment')
expect(rendered).to have_link('Delete comment')
end
- it 'shows Edit and Delete buttons if editable and current users comment' do
+ it 'shows Delete button if editable and current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: author_user, note_editable: true, note: note
- expect(rendered).to have_button('Edit comment')
expect(rendered).to have_link('Delete comment')
end
end
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 33eba3e6d3d..3c25e341b39 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -12,6 +12,7 @@ describe 'projects/tree/show' do
allow(view).to receive(:can?).and_return(true)
allow(view).to receive(:can_collaborate_with_project?).and_return(true)
+ allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
end
context 'for branch names ending on .json' do
diff --git a/spec/views/shared/projects/_project.html.haml_spec.rb b/spec/views/shared/projects/_project.html.haml_spec.rb
index b500016016a..f0a4f153699 100644
--- a/spec/views/shared/projects/_project.html.haml_spec.rb
+++ b/spec/views/shared/projects/_project.html.haml_spec.rb
@@ -3,6 +3,10 @@ require 'spec_helper'
describe 'shared/projects/_project.html.haml' do
let(:project) { create(:project) }
+ before do
+ allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
+ end
+
it 'should render creator avatar if project has a creator' do
render 'shared/projects/project', use_creator_avatar: true, project: project
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
index 03b9b99e263..90ed1309d4a 100644
--- a/spec/workers/authorized_projects_worker_spec.rb
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -3,47 +3,100 @@ require 'spec_helper'
describe AuthorizedProjectsWorker do
let(:project) { create(:project) }
+ def build_args_list(*ids, multiply: 1)
+ args_list = ids.map { |id| [id] }
+ args_list * multiply
+ end
+
describe '.bulk_perform_and_wait' do
it 'schedules the ids and waits for the jobs to complete' do
+ args_list = build_args_list(project.owner.id)
+
project.owner.project_authorizations.delete_all
+ described_class.bulk_perform_and_wait(args_list)
+
+ expect(project.owner.project_authorizations.count).to eq(1)
+ end
- described_class.bulk_perform_and_wait([[project.owner.id]])
+ it 'inlines workloads <= 3 jobs' do
+ args_list = build_args_list(project.owner.id, multiply: 3)
+ expect(described_class).to receive(:bulk_perform_inline).with(args_list)
+
+ described_class.bulk_perform_and_wait(args_list)
+ end
+
+ it 'runs > 3 jobs using sidekiq' do
+ project.owner.project_authorizations.delete_all
+
+ expect(described_class).to receive(:bulk_perform_async).and_call_original
+
+ args_list = build_args_list(project.owner.id, multiply: 4)
+ described_class.bulk_perform_and_wait(args_list)
expect(project.owner.project_authorizations.count).to eq(1)
end
end
+ describe '.bulk_perform_inline' do
+ it 'refreshes the authorizations inline' do
+ project.owner.project_authorizations.delete_all
+
+ expect_any_instance_of(described_class).to receive(:perform).and_call_original
+
+ described_class.bulk_perform_inline(build_args_list(project.owner.id))
+
+ expect(project.owner.project_authorizations.count).to eq(1)
+ end
+
+ it 'enqueues jobs if an error is raised' do
+ invalid_id = -1
+ args_list = build_args_list(project.owner.id, invalid_id)
+
+ allow_any_instance_of(described_class).to receive(:perform).with(project.owner.id)
+ allow_any_instance_of(described_class).to receive(:perform).with(invalid_id).and_raise(ArgumentError)
+ expect(described_class).to receive(:bulk_perform_async).with(build_args_list(invalid_id))
+
+ described_class.bulk_perform_inline(args_list)
+ end
+ end
+
describe '.bulk_perform_async' do
it "uses it's respective sidekiq queue" do
- args = [[project.owner.id]]
+ args_list = build_args_list(project.owner.id)
push_bulk_args = {
'class' => described_class,
'queue' => described_class.sidekiq_options['queue'],
- 'args' => args
+ 'args' => args_list
}
expect(Sidekiq::Client).to receive(:push_bulk).with(push_bulk_args).once
- described_class.bulk_perform_async(args)
+ described_class.bulk_perform_async(args_list)
end
end
describe '#perform' do
- subject { described_class.new }
+ let(:user) { create(:user) }
- it "refreshes user's authorized projects" do
- user = create(:user)
+ subject(:job) { described_class.new }
+ it "refreshes user's authorized projects" do
expect_any_instance_of(User).to receive(:refresh_authorized_projects)
- subject.perform(user.id)
+ job.perform(user.id)
+ end
+
+ it 'notifies the JobWaiter when done if the key is provided' do
+ expect(Gitlab::JobWaiter).to receive(:notify).with('notify-key', job.jid)
+
+ job.perform(user.id, 'notify-key')
end
context "when the user is not found" do
it "does nothing" do
expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
- subject.perform(-1)
+ job.perform(-1)
end
end
end
diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb
index 2868167c7d4..8cc3f37ebe8 100644
--- a/spec/workers/build_finished_worker_spec.rb
+++ b/spec/workers/build_finished_worker_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe BuildFinishedWorker do
describe '#perform' do
context 'when build exists' do
- let(:build) { create(:ci_build) }
+ let!(:build) { create(:ci_build) }
it 'calculates coverage and calls hooks' do
expect(BuildCoverageWorker)
diff --git a/spec/workers/concerns/build_queue_spec.rb b/spec/workers/concerns/build_queue_spec.rb
deleted file mode 100644
index 6bf955e0be2..00000000000
--- a/spec/workers/concerns/build_queue_spec.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-require 'spec_helper'
-
-describe BuildQueue do
- let(:worker) do
- Class.new do
- include Sidekiq::Worker
- include BuildQueue
- end
- end
-
- it 'sets the queue name of a worker' do
- expect(worker.sidekiq_options['queue'].to_s).to eq('build')
- end
-end
diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb
index 40794d0e42a..eac5a770e5f 100644
--- a/spec/workers/concerns/pipeline_queue_spec.rb
+++ b/spec/workers/concerns/pipeline_queue_spec.rb
@@ -8,7 +8,17 @@ describe PipelineQueue do
end
end
- it 'sets the queue name of a worker' do
- expect(worker.sidekiq_options['queue'].to_s).to eq('pipeline')
+ it 'sets a default pipelines queue automatically' do
+ expect(worker.sidekiq_options['queue'])
+ .to eq 'pipeline_default'
+ end
+
+ describe '.enqueue_in' do
+ it 'sets a custom sidekiq queue with prefix and group' do
+ worker.enqueue_in(group: :processing)
+
+ expect(worker.sidekiq_options['queue'])
+ .to eq 'pipeline_processing'
+ end
end
end
diff --git a/spec/workers/create_gpg_signature_worker_spec.rb b/spec/workers/create_gpg_signature_worker_spec.rb
index c6a17d77d73..aa6c347d738 100644
--- a/spec/workers/create_gpg_signature_worker_spec.rb
+++ b/spec/workers/create_gpg_signature_worker_spec.rb
@@ -1,34 +1,31 @@
require 'spec_helper'
describe CreateGpgSignatureWorker do
+ let(:project) { create(:project, :repository) }
+
context 'when GpgKey is found' do
- it 'calls Commit#signature' do
- commit_sha = '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
- project = create :project
+ let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
+
+ it 'calls Gitlab::Gpg::Commit#signature' do
commit = instance_double(Commit)
+ gpg_commit = instance_double(Gitlab::Gpg::Commit)
allow(Project).to receive(:find_by).with(id: project.id).and_return(project)
allow(project).to receive(:commit).with(commit_sha).and_return(commit)
- expect(commit).to receive(:signature)
+ expect(Gitlab::Gpg::Commit).to receive(:new).with(commit).and_return(gpg_commit)
+ expect(gpg_commit).to receive(:signature)
described_class.new.perform(commit_sha, project.id)
end
end
context 'when Commit is not found' do
- let(:nonexisting_commit_sha) { 'bogus' }
- let(:project) { create :project }
+ let(:nonexisting_commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a34' }
it 'does not raise errors' do
expect { described_class.new.perform(nonexisting_commit_sha, project.id) }.not_to raise_error
end
-
- it 'does not call Commit#signature' do
- expect_any_instance_of(Commit).not_to receive(:signature)
-
- described_class.new.perform(nonexisting_commit_sha, project.id)
- end
end
context 'when Project is not found' do
@@ -38,8 +35,8 @@ describe CreateGpgSignatureWorker do
expect { described_class.new.perform(anything, nonexisting_project_id) }.not_to raise_error
end
- it 'does not call Commit#signature' do
- expect_any_instance_of(Commit).not_to receive(:signature)
+ it 'does not call Gitlab::Gpg::Commit#signature' do
+ expect_any_instance_of(Gitlab::Gpg::Commit).not_to receive(:signature)
described_class.new.perform(anything, nonexisting_project_id)
end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 05f971dfd13..6f9ddb6c63c 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -5,28 +5,100 @@ require 'spec_helper'
describe GitGarbageCollectWorker do
let(:project) { create(:project, :repository) }
let(:shell) { Gitlab::Shell.new }
+ let!(:lease_uuid) { SecureRandom.uuid }
+ let!(:lease_key) { "project_housekeeping:#{project.id}" }
subject { described_class.new }
describe "#perform" do
shared_examples 'flushing ref caches' do |gitaly|
- it "flushes ref caches when the task if 'gc'" do
- expect(subject).to receive(:command).with(:gc).and_return([:the, :command])
-
- if gitaly
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
- .and_return(nil)
- else
- expect(Gitlab::Popen).to receive(:popen)
- .with([:the, :command], project.repository.path_to_repo).and_return(["", 0])
+ context 'with active lease_uuid' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
end
- 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(Repository).to receive(:branch_count).and_call_original
- expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
+ it "flushes ref caches when the task if 'gc'" do
+ expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original
+ expect(subject).to receive(:command).with(:gc).and_return([:the, :command])
- subject.perform(project.id)
+ if gitaly
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
+ .and_return(nil)
+ else
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([:the, :command], project.repository.path_to_repo).and_return(["", 0])
+ end
+
+ 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(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(project.id, :gc, lease_key, lease_uuid)
+ end
+ end
+
+ context 'with different lease than the active one' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid)
+ end
+
+ it 'returns silently' 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)
+ end
+ end
+
+ context 'with no active lease' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(false)
+ end
+
+ context 'when is able to get the lease' do
+ before do
+ allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
+ end
+
+ it "flushes ref caches when the task if 'gc'" do
+ expect(subject).to receive(:command).with(:gc).and_return([:the, :command])
+
+ if gitaly
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
+ .and_return(nil)
+ else
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([:the, :command], project.repository.path_to_repo).and_return(["", 0])
+ end
+
+ 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(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(project.id)
+ end
+ end
+
+ context 'when no lease can be obtained' do
+ before do
+ expect(subject).to receive(:try_obtain_lease).and_return(false)
+ end
+
+ it 'returns silently' 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)
+ end
+ end
end
end
@@ -39,25 +111,34 @@ describe GitGarbageCollectWorker do
end
context "repack_full" do
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
it "calls Gitaly" do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_full)
.and_return(nil)
- subject.perform(project.id, :full_repack)
+ subject.perform(project.id, :full_repack, lease_key, lease_uuid)
end
end
context "repack_incremental" do
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
it "calls Gitaly" do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_incremental)
.and_return(nil)
- subject.perform(project.id, :incremental_repack)
+ subject.perform(project.id, :incremental_repack, lease_key, lease_uuid)
end
end
shared_examples 'gc tasks' do
before do
+ allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled)
end
@@ -67,7 +148,7 @@ describe GitGarbageCollectWorker do
expect(before_packs.count).to be >= 1
- subject.perform(project.id, 'incremental_repack')
+ subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid)
after_packs = packs(project)
# Exactly one new pack should have been created
@@ -79,12 +160,12 @@ describe GitGarbageCollectWorker do
it 'full repack consolidates into 1 packfile' do
create_objects(project)
- subject.perform(project.id, 'incremental_repack')
+ subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid)
before_packs = packs(project)
expect(before_packs.count).to be >= 2
- subject.perform(project.id, 'full_repack')
+ subject.perform(project.id, 'full_repack', lease_key, lease_uuid)
after_packs = packs(project)
expect(after_packs.count).to eq(1)
@@ -102,7 +183,7 @@ describe GitGarbageCollectWorker do
expect(before_packs.count).to be >= 1
- subject.perform(project.id, 'gc')
+ subject.perform(project.id, 'gc', lease_key, lease_uuid)
after_packed_refs = packed_refs(project)
after_packs = packs(project)
@@ -143,7 +224,7 @@ describe GitGarbageCollectWorker do
tree: old_commit.tree,
parents: [old_commit]
)
- GitOperationService.new(nil, project.repository).send(
+ Gitlab::Git::OperationService.new(nil, project.repository.raw_repository).send(
:update_ref,
"refs/heads/#{SecureRandom.hex(6)}",
new_commit_sha,
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index ee51000161a..303193bab9b 100644
--- a/spec/workers/merge_worker_spec.rb
+++ b/spec/workers/merge_worker_spec.rb
@@ -27,15 +27,4 @@ describe MergeWorker do
expect(source_project.repository.branch_names).not_to include('markdown')
end
end
-
- it 'persists merge_jid' do
- merge_request = create(:merge_request, merge_jid: nil)
- user = create(:user)
- worker = described_class.new
-
- allow(worker).to receive(:jid) { '999' }
-
- expect { worker.perform(merge_request.id, user.id, {}) }
- .to change { merge_request.reload.merge_jid }.from(nil).to('999')
- end
end
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
index f2706254284..20cf580af8a 100644
--- a/spec/workers/namespaceless_project_destroy_worker_spec.rb
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -5,7 +5,7 @@ describe NamespacelessProjectDestroyWorker do
before do
# Stub after_save callbacks that will fail when Project has no namespace
- allow_any_instance_of(Project).to receive(:ensure_storage_path_exist).and_return(nil)
+ allow_any_instance_of(Project).to receive(:ensure_storage_path_exists).and_return(nil)
allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil)
end
@@ -75,5 +75,19 @@ describe NamespacelessProjectDestroyWorker do
end
end
end
+
+ context 'project has non-existing namespace' do
+ let!(:project) do
+ project = build(:project, namespace_id: Namespace.maximum(:id).to_i.succ)
+ project.save(validate: false)
+ project
+ end
+
+ it 'deletes the project' do
+ subject.perform(project.id)
+
+ expect(Project.unscoped.all).not_to include(project)
+ end
+ end
end
end
diff --git a/spec/workers/new_issue_worker_spec.rb b/spec/workers/new_issue_worker_spec.rb
index ed49ce57c0b..4e15ccc534b 100644
--- a/spec/workers/new_issue_worker_spec.rb
+++ b/spec/workers/new_issue_worker_spec.rb
@@ -41,7 +41,7 @@ describe NewIssueWorker do
let(:issue) { create(:issue, project: project, description: "issue for #{mentioned.to_reference}") }
it 'creates a new event record' do
- expect{ worker.perform(issue.id, user.id) }.to change { Event.count }.from(0).to(1)
+ expect { worker.perform(issue.id, user.id) }.to change { Event.count }.from(0).to(1)
end
it 'creates a notification for the assignee' do
diff --git a/spec/workers/new_merge_request_worker_spec.rb b/spec/workers/new_merge_request_worker_spec.rb
index 85af6184d39..9e0cbde45b1 100644
--- a/spec/workers/new_merge_request_worker_spec.rb
+++ b/spec/workers/new_merge_request_worker_spec.rb
@@ -43,7 +43,7 @@ describe NewMergeRequestWorker do
end
it 'creates a new event record' do
- expect{ worker.perform(merge_request.id, user.id) }.to change { Event.count }.from(0).to(1)
+ expect { worker.perform(merge_request.id, user.id) }.to change { Event.count }.from(0).to(1)
end
it 'creates a notification for the assignee' do
diff --git a/spec/workers/pipeline_metrics_worker_spec.rb b/spec/workers/pipeline_metrics_worker_spec.rb
index ef71125c0b6..896f9e6e7f2 100644
--- a/spec/workers/pipeline_metrics_worker_spec.rb
+++ b/spec/workers/pipeline_metrics_worker_spec.rb
@@ -2,7 +2,12 @@ require 'spec_helper'
describe PipelineMetricsWorker do
let(:project) { create(:project, :repository) }
- let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref, head_pipeline: pipeline) }
+
+ let!(:merge_request) do
+ create(:merge_request, source_project: project,
+ source_branch: pipeline.ref,
+ head_pipeline: pipeline)
+ end
let(:pipeline) do
create(:ci_empty_pipeline,
@@ -14,6 +19,8 @@ describe PipelineMetricsWorker do
finished_at: Time.now)
end
+ let(:status) { 'pending' }
+
describe '#perform' do
before do
described_class.new.perform(pipeline.id)
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index 14ed8b7811e..75197039f5a 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -23,7 +23,7 @@ describe PipelineScheduleWorker do
context 'when there is a scheduled pipeline within next_run_at' do
it 'creates a new pipeline' do
- expect{ subject }.to change { project.pipelines.count }.by(1)
+ expect { subject }.to change { project.pipelines.count }.by(1)
expect(Ci::Pipeline.last).to be_schedule
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 74a9f90195c..d3707a3cc11 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -78,7 +78,7 @@ describe PostReceive do
stub_ci_pipeline_to_return_yaml_file
end
- it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) }
+ it { expect { subject }.to change { Ci::Pipeline.count }.by(2) }
end
context "does not create a Ci::Pipeline" do
@@ -86,7 +86,7 @@ describe PostReceive do
stub_ci_pipeline_yaml_file(nil)
end
- it { expect{ subject }.not_to change{ Ci::Pipeline.count } }
+ it { expect { subject }.not_to change { Ci::Pipeline.count } }
end
end
@@ -127,6 +127,7 @@ describe PostReceive do
it "asks the project to trigger all hooks" do
allow(Project).to receive(:find_by).and_return(project)
+
expect(project).to receive(:execute_hooks).twice
expect(project).to receive(:execute_services).twice
@@ -135,6 +136,7 @@ describe PostReceive do
it "enqueues a UpdateMergeRequestsWorker job" do
allow(Project).to receive(:find_by).and_return(project)
+
expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args)
described_class.new.perform(gl_repository, key_id, base64_changes)
diff --git a/spec/workers/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb
index 35e1518a35e..ea974355050 100644
--- a/spec/workers/prune_old_events_worker_spec.rb
+++ b/spec/workers/prune_old_events_worker_spec.rb
@@ -2,9 +2,11 @@ require 'spec_helper'
describe PruneOldEventsWorker do
describe '#perform' do
- let!(:expired_event) { create(:event, author_id: 0, created_at: 13.months.ago) }
- let!(:not_expired_event) { create(:event, author_id: 0, created_at: 1.day.ago) }
- let!(:exactly_12_months_event) { create(:event, author_id: 0, created_at: 12.months.ago) }
+ let(:user) { create(:user) }
+
+ let!(:expired_event) { create(:event, :closed, author: user, created_at: 13.months.ago) }
+ let!(:not_expired_event) { create(:event, :closed, author: user, created_at: 1.day.ago) }
+ let!(:exactly_12_months_event) { create(:event, :closed, author: user, created_at: 12.months.ago) }
it 'prunes events older than 12 months' do
expect { subject.perform }.to change { Event.count }.by(-1)
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index ca904e512ac..100dfc32bbe 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -22,8 +22,8 @@ describe RepositoryImportWorker do
it 'hide the credentials that were used in the import URL' do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
+ project.update_attributes(import_jid: '123')
expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error })
- allow(subject).to receive(:jid).and_return('123')
expect do
subject.perform(project.id)
diff --git a/spec/workers/stage_update_worker_spec.rb b/spec/workers/stage_update_worker_spec.rb
new file mode 100644
index 00000000000..7bc76c79464
--- /dev/null
+++ b/spec/workers/stage_update_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe StageUpdateWorker do
+ describe '#perform' do
+ context 'when stage exists' do
+ let(:stage) { create(:ci_stage_entity) }
+
+ it 'updates stage status' do
+ expect_any_instance_of(Ci::Stage).to receive(:update_status)
+
+ described_class.new.perform(stage.id)
+ end
+ end
+
+ context 'when stage 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/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb
index 549635f7f33..ac6f4fefb4e 100644
--- a/spec/workers/stuck_ci_jobs_worker_spec.rb
+++ b/spec/workers/stuck_ci_jobs_worker_spec.rb
@@ -6,27 +6,31 @@ describe StuckCiJobsWorker do
let(:worker) { described_class.new }
let(:exclusive_lease_uuid) { SecureRandom.uuid }
- subject do
- job.reload
- job.status
- end
-
before do
job.update!(status: status, updated_at: updated_at)
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid)
end
shared_examples 'job is dropped' do
- it 'changes status' do
+ before do
worker.perform
- is_expected.to eq('failed')
+ job.reload
+ end
+
+ it "changes status" do
+ expect(job).to be_failed
+ expect(job).to be_stuck_or_timeout_failure
end
end
shared_examples 'job is unchanged' do
- it "doesn't change status" do
+ before do
worker.perform
- is_expected.to eq(status)
+ job.reload
+ end
+
+ it "doesn't change status" do
+ expect(job.status).to eq(status)
end
end
diff --git a/spec/workers/stuck_import_jobs_worker_spec.rb b/spec/workers/stuck_import_jobs_worker_spec.rb
index 2f5b685a332..a82eb54ffe4 100644
--- a/spec/workers/stuck_import_jobs_worker_spec.rb
+++ b/spec/workers/stuck_import_jobs_worker_spec.rb
@@ -8,29 +8,29 @@ describe StuckImportJobsWorker do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid)
end
- describe 'long running import' do
- let(:project) { create(:project, import_jid: '123', import_status: 'started') }
+ describe 'with started import_status' do
+ let(:project) { create(:project, :import_started, import_jid: '123') }
- before do
- allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(['123'])
- end
+ describe 'long running import' do
+ it 'marks the project as failed' do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(['123'])
- it 'marks the project as failed' do
- expect { worker.perform }.to change { project.reload.import_status }.to('failed')
+ expect { worker.perform }.to change { project.reload.import_status }.to('failed')
+ end
end
- end
- describe 'running import' do
- let(:project) { create(:project, import_jid: '123', import_status: 'started') }
-
- before do
- allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([])
- end
+ describe 'running import' do
+ it 'does not mark the project as failed' do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([])
- it 'does not mark the project as failed' do
- worker.perform
+ expect { worker.perform }.not_to change { project.reload.import_status }
+ end
- expect(project.reload.import_status).to eq('started')
+ describe 'import without import_jid' do
+ it 'marks the project as failed' do
+ expect { worker.perform }.to change { project.reload.import_status }.to('failed')
+ end
+ end
end
end
end
diff --git a/vendor/Dockerfile/CONTRIBUTING.md b/vendor/Dockerfile/CONTRIBUTING.md
index 91b92eafa1b..0878db6dd9e 100644
--- a/vendor/Dockerfile/CONTRIBUTING.md
+++ b/vendor/Dockerfile/CONTRIBUTING.md
@@ -3,3 +3,50 @@ https://gitlab.com/gitlab-org/Dockerfile.
GitLab only mirrors the templates. Please submit your merge requests to
https://gitlab.com/gitlab-org/Dockerfile.
+
+## 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
diff --git a/vendor/assets/stylesheets/katex.scss b/vendor/assets/stylesheets/katex.scss
index 9dd8a30bf51..b45836716f2 100644
--- a/vendor/assets/stylesheets/katex.scss
+++ b/vendor/assets/stylesheets/katex.scss
@@ -45,112 +45,112 @@ SOFTWARE.
font-family: 'KaTeX_AMS';
src: url(font-path('KaTeX_AMS-Regular.eot'));
src: url(font-path('KaTeX_AMS-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_AMS-Regular.woff2')) format('woff2'), url(font-path('KaTeX_AMS-Regular.woff')) format('woff'), url(font-path('KaTeX_AMS-Regular.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Caligraphic';
src: url(font-path('KaTeX_Caligraphic-Bold.eot'));
src: url(font-path('KaTeX_Caligraphic-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Caligraphic-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Caligraphic-Bold.woff')) format('woff'), url(font-path('KaTeX_Caligraphic-Bold.ttf')) format('truetype');
- font-weight: bold;
+ font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Caligraphic';
src: url(font-path('KaTeX_Caligraphic-Regular.eot'));
src: url(font-path('KaTeX_Caligraphic-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Caligraphic-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Caligraphic-Regular.woff')) format('woff'), url(font-path('KaTeX_Caligraphic-Regular.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Fraktur';
src: url(font-path('KaTeX_Fraktur-Bold.eot'));
src: url(font-path('KaTeX_Fraktur-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Fraktur-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Fraktur-Bold.woff')) format('woff'), url(font-path('KaTeX_Fraktur-Bold.ttf')) format('truetype');
- font-weight: bold;
+ font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Fraktur';
src: url(font-path('KaTeX_Fraktur-Regular.eot'));
src: url(font-path('KaTeX_Fraktur-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Fraktur-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Fraktur-Regular.woff')) format('woff'), url(font-path('KaTeX_Fraktur-Regular.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Main';
src: url(font-path('KaTeX_Main-Bold.eot'));
src: url(font-path('KaTeX_Main-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Main-Bold.woff')) format('woff'), url(font-path('KaTeX_Main-Bold.ttf')) format('truetype');
- font-weight: bold;
+ font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Main';
src: url(font-path('KaTeX_Main-Italic.eot'));
src: url(font-path('KaTeX_Main-Italic.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Italic.woff2')) format('woff2'), url(font-path('KaTeX_Main-Italic.woff')) format('woff'), url(font-path('KaTeX_Main-Italic.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'KaTeX_Main';
src: url(font-path('KaTeX_Main-Regular.eot'));
src: url(font-path('KaTeX_Main-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Main-Regular.woff')) format('woff'), url(font-path('KaTeX_Main-Regular.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Math';
src: url(font-path('KaTeX_Math-Italic.eot'));
src: url(font-path('KaTeX_Math-Italic.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Math-Italic.woff2')) format('woff2'), url(font-path('KaTeX_Math-Italic.woff')) format('woff'), url(font-path('KaTeX_Math-Italic.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: italic;
}
@font-face {
font-family: 'KaTeX_SansSerif';
src: url(font-path('KaTeX_SansSerif-Regular.eot'));
src: url(font-path('KaTeX_SansSerif-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_SansSerif-Regular.woff2')) format('woff2'), url(font-path('KaTeX_SansSerif-Regular.woff')) format('woff'), url(font-path('KaTeX_SansSerif-Regular.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Script';
src: url(font-path('KaTeX_Script-Regular.eot'));
src: url(font-path('KaTeX_Script-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Script-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Script-Regular.woff')) format('woff'), url(font-path('KaTeX_Script-Regular.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Size1';
src: url(font-path('KaTeX_Size1-Regular.eot'));
src: url(font-path('KaTeX_Size1-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size1-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size1-Regular.woff')) format('woff'), url(font-path('KaTeX_Size1-Regular.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Size2';
src: url(font-path('KaTeX_Size2-Regular.eot'));
src: url(font-path('KaTeX_Size2-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size2-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size2-Regular.woff')) format('woff'), url(font-path('KaTeX_Size2-Regular.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Size3';
src: url(font-path('KaTeX_Size3-Regular.eot'));
src: url(font-path('KaTeX_Size3-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size3-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size3-Regular.woff')) format('woff'), url(font-path('KaTeX_Size3-Regular.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Size4';
src: url(font-path('KaTeX_Size4-Regular.eot'));
src: url(font-path('KaTeX_Size4-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size4-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size4-Regular.woff')) format('woff'), url(font-path('KaTeX_Size4-Regular.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'KaTeX_Typewriter';
src: url(font-path('KaTeX_Typewriter-Regular.eot'));
src: url(font-path('KaTeX_Typewriter-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Typewriter-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Typewriter-Regular.woff')) format('woff'), url(font-path('KaTeX_Typewriter-Regular.ttf')) format('truetype');
- font-weight: normal;
+ font-weight: 400;
font-style: normal;
}
.katex-display {
@@ -192,7 +192,7 @@ SOFTWARE.
}
.katex .mathbf {
font-family: KaTeX_Main;
- font-weight: bold;
+ font-weight: 600;
}
.katex .amsrm {
font-family: KaTeX_AMS;
diff --git a/vendor/assets/stylesheets/xterm/xterm.css b/vendor/assets/stylesheets/xterm/xterm.css
index b30d7b493f1..fabc51b0e3d 100644
--- a/vendor/assets/stylesheets/xterm/xterm.css
+++ b/vendor/assets/stylesheets/xterm/xterm.css
@@ -142,7 +142,7 @@
* Determine default colors for xterm.js
*/
.terminal .xterm-bold {
- font-weight: bold;
+ font-weight: 600;
}
.terminal .xterm-underline {
diff --git a/vendor/gitignore/Actionscript.gitignore b/vendor/gitignore/Actionscript.gitignore
index 11e612e9853..5d947ca8879 100644
--- a/vendor/gitignore/Actionscript.gitignore
+++ b/vendor/gitignore/Actionscript.gitignore
@@ -1,9 +1,8 @@
# Build and Release Folders
-bin/
bin-debug/
bin-release/
-[Oo]bj/ # FlashDevelop obj
-[Bb]in/ # FlashDevelop bin
+[Oo]bj/
+[Bb]in/
# Other files and folders
.settings/
diff --git a/vendor/gitignore/Drupal.gitignore b/vendor/gitignore/Drupal.gitignore
index 0d2fe537f46..072b683190f 100644
--- a/vendor/gitignore/Drupal.gitignore
+++ b/vendor/gitignore/Drupal.gitignore
@@ -1,10 +1,12 @@
# Ignore configuration files that may contain sensitive information.
sites/*/*settings*.php
+sites/example.sites.php
# Ignore paths that contain generated content.
files/
sites/*/files
sites/*/private
+sites/*/translations
# Ignore default text files
robots.txt
@@ -16,6 +18,7 @@ robots.txt
/UPGRADE.txt
/README.txt
sites/README.txt
+sites/all/libraries/README.txt
sites/all/modules/README.txt
sites/all/themes/README.txt
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index ff23445e2b0..345e61ae3f2 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -31,7 +31,7 @@ cmake-build-debug/
## Plugin-specific files:
# IntelliJ
-/out/
+out/
# mpeltonen/sbt-idea plugin
.idea_modules/
diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore
index 450f32ec40c..eee88b2f0f7 100644
--- a/vendor/gitignore/Haskell.gitignore
+++ b/vendor/gitignore/Haskell.gitignore
@@ -18,3 +18,4 @@ cabal.sandbox.config
.stack-work/
cabal.project.local
.HTF/
+.ghc.environment.*
diff --git a/vendor/gitignore/Kotlin.gitignore b/vendor/gitignore/Kotlin.gitignore
new file mode 120000
index 00000000000..c48376eebcf
--- /dev/null
+++ b/vendor/gitignore/Kotlin.gitignore
@@ -0,0 +1 @@
+Java.gitignore \ No newline at end of file
diff --git a/vendor/gitignore/Nanoc.gitignore b/vendor/gitignore/Nanoc.gitignore
index 3f36ea2a878..6f35daaf478 100644
--- a/vendor/gitignore/Nanoc.gitignore
+++ b/vendor/gitignore/Nanoc.gitignore
@@ -4,7 +4,7 @@
output/
# Temporary file directory
-tmp/
+tmp/nanoc/
# Crash Log
crash.log
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index 00cbbdf53f6..97e28736892 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -29,7 +29,7 @@ bower_components
# node-waf configuration
.lock-wscript
-# Compiled binary addons (http://nodejs.org/api/addons.html)
+# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
diff --git a/vendor/gitignore/Prestashop.gitignore b/vendor/gitignore/Prestashop.gitignore
index 7c6ae1e31cc..81f45e19eba 100644
--- a/vendor/gitignore/Prestashop.gitignore
+++ b/vendor/gitignore/Prestashop.gitignore
@@ -7,8 +7,10 @@ config/settings.*.php
# The following files are generated by PrestaShop.
admin-dev/autoupgrade/
-/cache/
+/cache/*
!/cache/index.php
+!/cache/*/
+/cache/*/*
!/cache/cachefs/index.php
!/cache/purifier/index.php
!/cache/push/index.php
diff --git a/vendor/gitignore/Qt.gitignore b/vendor/gitignore/Qt.gitignore
index 5fa47c5a1f2..fe67fdf1ee6 100644
--- a/vendor/gitignore/Qt.gitignore
+++ b/vendor/gitignore/Qt.gitignore
@@ -26,6 +26,8 @@ moc_*.cpp
moc_*.h
qrc_*.cpp
ui_*.h
+*.qmlc
+*.jsc
Makefile*
*build-*
diff --git a/vendor/gitignore/Smalltalk.gitignore b/vendor/gitignore/Smalltalk.gitignore
index 75272b23472..943995e1172 100644
--- a/vendor/gitignore/Smalltalk.gitignore
+++ b/vendor/gitignore/Smalltalk.gitignore
@@ -13,6 +13,10 @@ SqueakDebug.log
# Monticello package cache
/package-cache
+# playground cache
+/play-cache
+/play-stash
+
# Metacello-github cache
/github-cache
github-*.zip
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index d5340449396..161179bff3e 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -37,6 +37,7 @@ playground.xcworkspace
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
+# Package.resolved
.build/
# CocoaPods
diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore
index 6c224e024e9..85fd714a965 100644
--- a/vendor/gitignore/Symfony.gitignore
+++ b/vendor/gitignore/Symfony.gitignore
@@ -39,3 +39,6 @@
# Backup entities generated with doctrine:generate:entities command
**/Entity/*~
+
+# Embedded web-server pid file
+/.web-server-pid
diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore
index 41859c81f1c..f20453be963 100644
--- a/vendor/gitignore/Terraform.gitignore
+++ b/vendor/gitignore/Terraform.gitignore
@@ -1,5 +1,6 @@
# Compiled files
*.tfstate
+*.tfstate.*.backup
*.tfstate.backup
# Module directory
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 22fd88a55a3..f652b45c2ee 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -116,6 +116,10 @@ _TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
# Visual Studio code coverage results
*.coverage
*.coveragexml
@@ -151,7 +155,7 @@ publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
-# TODO: Comment the next line if you want to checkin your web deploy settings
+# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
new file mode 100644
index 00000000000..a07edd264c3
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -0,0 +1,411 @@
+# Auto DevOps
+# This CI/CD configuration provides a standard pipeline for
+# * building a Docker image (using a buildpack if necessary),
+# * storing the image in the container registry,
+# * running tests from a buildpack,
+# * running code quality analysis,
+# * creating a review app for each topic branch,
+# * and continuous deployment to production
+#
+# In order to deploy, you must have a Kubernetes cluster configured either
+# via a project integration, or via group/project variables.
+# AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project
+# level, or manually added below.
+#
+# If you want to deploy to staging first, or enable canary deploys,
+# uncomment the relevant jobs in the pipeline below.
+#
+# If Auto DevOps fails to detect the proper buildpack, or if you want to
+# specify a custom buildpack, set a project variable `BUILDPACK_URL` to the
+# repository URL of the buildpack.
+# e.g. BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-ruby.git#v142
+# If you need multiple buildpacks, add a file to your project called
+# `.buildpacks` that contains the URLs, one on each line, in order.
+# Note: Auto CI does not work with multiple buildpacks yet
+
+image: alpine:latest
+
+variables:
+ # AUTO_DEVOPS_DOMAIN is the application deployment domain and should be set as a variable at the group or project level.
+ # AUTO_DEVOPS_DOMAIN: domain.example.com
+
+ POSTGRES_USER: user
+ POSTGRES_PASSWORD: testing-password
+ POSTGRES_ENABLED: "true"
+ POSTGRES_DB: $CI_ENVIRONMENT_SLUG
+
+stages:
+ - build
+ - test
+ - review
+ - staging
+ - canary
+ - production
+ - cleanup
+
+build:
+ stage: build
+ image: docker:git
+ services:
+ - docker:dind
+ variables:
+ DOCKER_DRIVER: overlay2
+ script:
+ - setup_docker
+ - build
+ only:
+ - branches
+
+test:
+ services:
+ - postgres:latest
+ variables:
+ POSTGRES_DB: test
+ stage: test
+ image: gliderlabs/herokuish:latest
+ script:
+ - setup_test_db
+ - cp -R . /tmp/app
+ - /bin/herokuish buildpack test
+ only:
+ - branches
+
+codequality:
+ image: docker:latest
+ variables:
+ DOCKER_DRIVER: overlay2
+ allow_failure: true
+ services:
+ - docker:dind
+ script:
+ - setup_docker
+ - codeclimate
+ artifacts:
+ paths: [codeclimate.json]
+
+review:
+ stage: review
+ script:
+ - check_kube_domain
+ - install_dependencies
+ - download_chart
+ - ensure_namespace
+ - install_tiller
+ - create_secret
+ - deploy
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$AUTO_DEVOPS_DOMAIN
+ on_stop: stop_review
+ only:
+ refs:
+ - branches
+ kubernetes: active
+ except:
+ - master
+
+stop_review:
+ stage: cleanup
+ variables:
+ GIT_STRATEGY: none
+ script:
+ - install_dependencies
+ - delete
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ action: stop
+ when: manual
+ allow_failure: true
+ only:
+ refs:
+ - branches
+ kubernetes: active
+ except:
+ - master
+
+# Keys that start with a dot (.) will not be processed by GitLab CI.
+# Staging and canary jobs are disabled by default, to enable them
+# remove the dot (.) before the job name.
+# https://docs.gitlab.com/ee/ci/yaml/README.html#hidden-keys
+
+# Staging deploys are disabled by default since
+# continuous deployment to production is enabled by default
+# If you prefer to automatically deploy to staging and
+# only manually promote to production, enable this job by removing the dot (.),
+# and uncomment the `when: manual` line in the `production` job.
+
+.staging:
+ stage: staging
+ script:
+ - check_kube_domain
+ - install_dependencies
+ - download_chart
+ - ensure_namespace
+ - install_tiller
+ - create_secret
+ - deploy
+ environment:
+ name: staging
+ url: http://$CI_PROJECT_PATH_SLUG-staging.$AUTO_DEVOPS_DOMAIN
+ only:
+ refs:
+ - master
+ kubernetes: active
+
+# Canaries are disabled by default, but if you want them,
+# and know what the downsides are, enable this job by removing the dot (.),
+# and uncomment the `when: manual` line in the `production` job.
+
+.canary:
+ stage: canary
+ script:
+ - check_kube_domain
+ - install_dependencies
+ - download_chart
+ - ensure_namespace
+ - install_tiller
+ - create_secret
+ - deploy canary
+ environment:
+ name: production
+ url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN
+ when: manual
+ only:
+ refs:
+ - master
+ kubernetes: active
+
+# This job continuously deploys to production on every push to `master`.
+# To make this a manual process, either because you're enabling `staging`
+# or `canary` deploys, or you simply want more control over when you deploy
+# to production, uncomment the `when: manual` line in the `production` job.
+
+production:
+ stage: production
+ script:
+ - check_kube_domain
+ - install_dependencies
+ - download_chart
+ - ensure_namespace
+ - install_tiller
+ - create_secret
+ - deploy
+ - delete canary
+ environment:
+ name: production
+ url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN
+# when: manual
+ only:
+ refs:
+ - master
+ kubernetes: active
+
+# ---------------------------------------------------------------------------
+
+.auto_devops: &auto_devops |
+ # Auto DevOps variables and functions
+ [[ "$TRACE" ]] && set -x
+ auto_database_url=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${CI_ENVIRONMENT_SLUG}-postgres:5432/${POSTGRES_DB}
+ export DATABASE_URL=${DATABASE_URL-$auto_database_url}
+ export CI_APPLICATION_REPOSITORY=$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
+ export CI_APPLICATION_TAG=$CI_COMMIT_SHA
+ export CI_CONTAINER_NAME=ci_job_build_${CI_JOB_ID}
+ export TILLER_NAMESPACE=$KUBE_NAMESPACE
+
+ function codeclimate() {
+ cc_opts="--env CODECLIMATE_CODE="$PWD" \
+ --volume "$PWD":/code \
+ --volume /var/run/docker.sock:/var/run/docker.sock \
+ --volume /tmp/cc:/tmp/cc"
+
+ docker run ${cc_opts} codeclimate/codeclimate init
+ docker run ${cc_opts} codeclimate/codeclimate analyze -f json > codeclimate.json
+ }
+
+ function deploy() {
+ track="${1-stable}"
+ name="$CI_ENVIRONMENT_SLUG"
+
+ if [[ "$track" != "stable" ]]; then
+ name="$name-$track"
+ fi
+
+ replicas="1"
+ service_enabled="false"
+ postgres_enabled="$POSTGRES_ENABLED"
+ # canary uses stable db
+ [[ "$track" == "canary" ]] && postgres_enabled="false"
+
+ env_track=$( echo $track | tr -s '[:lower:]' '[:upper:]' )
+ env_slug=$( echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]' )
+
+ if [[ "$track" == "stable" ]]; then
+ # for stable track get number of replicas from `PRODUCTION_REPLICAS`
+ eval new_replicas=\$${env_slug}_REPLICAS
+ service_enabled="true"
+ else
+ # for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS`
+ eval new_replicas=\$${env_track}_${env_slug}_REPLICAS
+ fi
+ if [[ -n "$new_replicas" ]]; then
+ replicas="$new_replicas"
+ fi
+
+ helm upgrade --install \
+ --wait \
+ --set service.enabled="$service_enabled" \
+ --set releaseOverride="$CI_ENVIRONMENT_SLUG" \
+ --set image.repository="$CI_APPLICATION_REPOSITORY" \
+ --set image.tag="$CI_APPLICATION_TAG" \
+ --set image.pullPolicy=IfNotPresent \
+ --set application.track="$track" \
+ --set application.database_url="$DATABASE_URL" \
+ --set service.url="$CI_ENVIRONMENT_URL" \
+ --set replicaCount="$replicas" \
+ --set postgresql.enabled="$postgres_enabled" \
+ --set postgresql.nameOverride="postgres" \
+ --set postgresql.postgresUser="$POSTGRES_USER" \
+ --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \
+ --set postgresql.postgresDatabase="$POSTGRES_DB" \
+ --namespace="$KUBE_NAMESPACE" \
+ --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() {
+ apk add -U openssl curl tar gzip bash ca-certificates git
+ wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://raw.githubusercontent.com/sgerrand/alpine-pkg-glibc/master/sgerrand.rsa.pub
+ wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.23-r3/glibc-2.23-r3.apk
+ apk add glibc-2.23-r3.apk
+ rm glibc-2.23-r3.apk
+
+ curl https://kubernetes-helm.storage.googleapis.com/helm-v2.6.1-linux-amd64.tar.gz | tar zx
+ mv linux-amd64/helm /usr/bin/
+ helm version --client
+
+ curl -L -o /usr/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
+ chmod +x /usr/bin/kubectl
+ kubectl version --client
+ }
+
+ function setup_docker() {
+ if ! docker info &>/dev/null; then
+ if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
+ export DOCKER_HOST='tcp://localhost:2375'
+ fi
+ fi
+ }
+
+ function setup_test_db() {
+ if [ -z ${KUBERNETES_PORT+x} ]; then
+ DB_HOST=postgres
+ else
+ DB_HOST=localhost
+ fi
+ export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:5432/${POSTGRES_DB}"
+ }
+
+ function download_chart() {
+ if [[ ! -d chart ]]; then
+ auto_chart=${AUTO_DEVOPS_CHART:-gitlab/auto-deploy-app}
+ auto_chart_name=$(basename $auto_chart)
+ auto_chart_name=${auto_chart_name%.tgz}
+ else
+ auto_chart="chart"
+ auto_chart_name="chart"
+ fi
+
+ helm init --client-only
+ helm repo add gitlab https://charts.gitlab.io
+ if [[ ! -d "$auto_chart" ]]; then
+ helm fetch ${auto_chart} --untar
+ fi
+ if [ "$auto_chart_name" != "chart" ]; then
+ mv ${auto_chart_name} chart
+ fi
+
+ helm dependency update chart/
+ helm dependency build chart/
+ }
+
+ function ensure_namespace() {
+ kubectl describe namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE"
+ }
+
+ function check_kube_domain() {
+ if [ -z ${AUTO_DEVOPS_DOMAIN+x} ]; then
+ echo "In order to deploy, AUTO_DEVOPS_DOMAIN must be set as a variable at the group or project level, or manually added in .gitlab-cy.yml"
+ false
+ else
+ true
+ fi
+ }
+
+ function build() {
+ if [[ -f Dockerfile ]]; then
+ echo "Building Dockerfile-based application..."
+ docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" .
+ else
+ echo "Building Heroku-based application using gliderlabs/herokuish docker image..."
+ docker run -i --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build
+ docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
+ docker rm "$CI_CONTAINER_NAME" >/dev/null
+ echo ""
+
+ echo "Configuring $CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG docker image..."
+ docker create --expose 5000 --env PORT=5000 --name="$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" /bin/herokuish procfile start web
+ docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
+ docker rm "$CI_CONTAINER_NAME" >/dev/null
+ echo ""
+ fi
+
+ if [[ -n "$CI_REGISTRY_USER" ]]; then
+ echo "Logging to GitLab Container Registry with CI credentials..."
+ docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
+ echo ""
+ fi
+
+ echo "Pushing to GitLab Container Registry..."
+ docker push "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG"
+ echo ""
+ }
+
+ function install_tiller() {
+ echo "Checking Tiller..."
+ helm init --upgrade
+ kubectl rollout status -n "$TILLER_NAMESPACE" -w "deployment/tiller-deploy"
+ if ! helm version --debug; then
+ echo "Failed to init Tiller."
+ return 1
+ fi
+ echo ""
+ }
+
+ function create_secret() {
+ kubectl create secret -n "$KUBE_NAMESPACE" \
+ docker-registry gitlab-registry \
+ --docker-server="$CI_REGISTRY" \
+ --docker-username="$CI_REGISTRY_USER" \
+ --docker-password="$CI_REGISTRY_PASSWORD" \
+ --docker-email="$GITLAB_USER_EMAIL" \
+ -o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f -
+ }
+
+ function delete() {
+ track="${1-stable}"
+ name="$CI_ENVIRONMENT_SLUG"
+
+ if [[ "$track" != "stable" ]]; then
+ name="$name-$track"
+ fi
+
+ helm delete "$name" || true
+ }
+
+before_script:
+ - *auto_devops
diff --git a/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml b/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml
index 27537689b80..2d218b2e164 100644
--- a/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml
@@ -4,32 +4,32 @@
image: busybox:latest
before_script:
- - echo "Before script section"
- - echo "For example you might run an update here or install a build dependency"
- - echo "Or perhaps you might print out some debugging details"
+ - echo "Before script section"
+ - echo "For example you might run an update here or install a build dependency"
+ - echo "Or perhaps you might print out some debugging details"
after_script:
- echo "After script section"
- echo "For example you might do some cleanup here"
build1:
- stage: build
- script:
- - echo "Do your build here"
+ stage: build
+ script:
+ - echo "Do your build here"
test1:
- stage: test
- script:
- - echo "Do a test here"
- - echo "For example run a test suite"
+ stage: test
+ script:
+ - echo "Do a test here"
+ - echo "For example run a test suite"
test2:
- stage: test
- script:
- - echo "Do another parallel test here"
- - echo "For example run a lint test"
+ stage: test
+ script:
+ - echo "Do another parallel test here"
+ - echo "For example run a lint test"
deploy1:
- stage: deploy
- script:
- - echo "Do your deploy here" \ No newline at end of file
+ stage: deploy
+ script:
+ - echo "Do your deploy here"
diff --git a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
index e23b6e212f0..8a214352d2a 100644
--- a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml
@@ -1,14 +1,19 @@
image: golang:latest
+variables:
+ # Please edit to your GitLab project
+ REPO_NAME: gitlab.com/namespace/project
+
# The problem is that to be able to use go get, one needs to put
# the repository in the $GOPATH. So for example if your gitlab domain
-# is mydomainperso.com, and that your repository is repos/projectname, and
+# is gitlab.com, and that your repository is namespace/project, and
# the default GOPATH being /go, then you'd need to have your
-# repository in /go/src/mydomainperso.com/repos/projectname
+# repository in /go/src/gitlab.com/namespace/project
# Thus, making a symbolic link corrects this.
before_script:
- - ln -s /builds /go/src/mydomainperso.com
- - cd /go/src/mydomainperso.com/repos/projectname
+ - mkdir -p $GOPATH/src/$REPO_NAME
+ - ln -svf $CI_PROJECT_DIR/* $GOPATH/src/$REPO_NAME
+ - cd $GOPATH/src/$REPO_NAME
stages:
- test
@@ -17,21 +22,14 @@ stages:
format:
stage: test
script:
- # Add here all the dependencies, or use glide/govendor to get
- # them automatically.
- # - curl https://glide.sh/get | sh
- - go get github.com/alecthomas/kingpin
- - go tool vet -composites=false -shadow=true *.go
- - go test -race $(go list ./... | grep -v /vendor/)
+ - go fmt $(go list ./... | grep -v /vendor/)
+ - go vet $(go list ./... | grep -v /vendor/)
+ - go test -race $(go list ./... | grep -v /vendor/)
compile:
stage: build
script:
- # Add here all the dependencies, or use glide/govendor/...
- # to get them automatically.
- - go get github.com/alecthomas/kingpin
- # Better put this in a Makefile
- - go build -race -ldflags "-extldflags '-static'" -o mybinary
+ - go build -race -ldflags "-extldflags '-static'" -o mybinary
artifacts:
- paths:
- - mybinary
+ paths:
+ - mybinary
diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
index a65e48a3389..48d98dddfad 100644
--- a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
@@ -1,41 +1,36 @@
-# This template uses the java:8 docker image because there isn't any
-# official Gradle image at this moment
-#
# This is the Gradle build system for JVM applications
# https://gradle.org/
# https://github.com/gradle/gradle
-image: java:8
+image: gradle:alpine
# Disable the Gradle daemon for Continuous Integration servers as correctness
# is usually a priority over speed in CI environments. Using a fresh
# runtime for each build is more reliable since the runtime is completely
# isolated from any previous builds.
variables:
- GRADLE_OPTS: "-Dorg.gradle.daemon=false"
+ GRADLE_OPTS: "-Dorg.gradle.daemon=false"
-# Make the gradle wrapper executable. This essentially downloads a copy of
-# Gradle to build the project with.
-# https://docs.gradle.org/current/userguide/gradle_wrapper.html
-# It is expected that any modern gradle project has a wrapper
before_script:
- - chmod +x gradlew
+ - export GRADLE_USER_HOME=`pwd`/.gradle
-# We redirect the gradle user home using -g so that it caches the
-# wrapper and dependencies.
-# https://docs.gradle.org/current/userguide/gradle_command_line.html
-#
-# Unfortunately it also caches the build output so
-# cleaning removes reminants of any cached builds.
-# The assemble task actually builds the project.
-# If it fails here, the tests can't run.
build:
stage: build
- script:
- - ./gradlew -g /cache/.gradle clean assemble
- allow_failure: false
+ script: gradle --build-cache assemble
+ cache:
+ key: "$CI_COMMIT_REF_NAME"
+ policy: push
+ paths:
+ - build
+ - .gradle
+
-# Use the generated build output to run the tests.
test:
stage: test
- script:
- - ./gradlew -g /cache/.gradle check
+ script: gradle check
+ cache:
+ key: "$CI_COMMIT_REF_NAME"
+ policy: pull
+ paths:
+ - build
+ - .gradle
+
diff --git a/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml
index 434de4f055a..0ad662cf704 100644
--- a/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml
@@ -34,6 +34,10 @@ before_script:
# Install php extensions
- docker-php-ext-install mbstring mcrypt pdo_mysql curl json intl gd xml zip bz2 opcache
+ # Install & enable Xdebug for code coverage reports
+ - pecl install xdebug
+ - docker-php-ext-enable xdebug
+
# Install Composer and project dependencies.
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install
diff --git a/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml b/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml
index bb8caa49d6b..33f44ee9222 100644
--- a/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml
@@ -11,6 +11,9 @@ before_script:
- apt-get install -yqq git libmcrypt-dev libpq-dev libcurl4-gnutls-dev libicu-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev
# Install PHP extensions
- docker-php-ext-install mbstring mcrypt pdo_pgsql curl json intl gd xml zip bz2 opcache
+# Install & enable Xdebug for code coverage reports
+- pecl install xdebug
+- docker-php-ext-enable xdebug
# Install and run Composer
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install
diff --git a/vendor/gitlab-ci-yml/Packer.gitlab-ci.yml b/vendor/gitlab-ci-yml/Packer.gitlab-ci.yml
new file mode 100644
index 00000000000..fa296057c72
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Packer.gitlab-ci.yml
@@ -0,0 +1,26 @@
+image:
+ name: hashicorp/packer:1.0.4
+ entrypoint:
+ - '/usr/bin/env'
+ - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
+
+before_script:
+ - packer --version
+
+stages:
+ - validate
+ - deploy
+
+validate:
+ stage: validate
+ script:
+ - find . -maxdepth 1 -name '*.json' -print0 | xargs -t0n1 packer validate
+
+build:
+ stage: deploy
+ environment: production
+ script:
+ - find . -maxdepth 1 -name '*.json' -print0 | xargs -t0n1 packer build
+ when: manual
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
index 4e181e85451..ff7bdd32239 100644
--- a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
@@ -1,6 +1,6 @@
# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/ruby/tags/
-image: "ruby:2.3"
+image: "ruby:2.4"
# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
@@ -40,9 +40,9 @@ rails:
variables:
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
script:
- - bundle exec rake db:migrate
- - bundle exec rake db:seed
- - bundle exec rake test
+ - rails db:migrate
+ - rails db:seed
+ - rails test
# This deploy job uses a simple deploy flow to Heroku, other providers, e.g. AWS Elastic Beanstalk
# are supported too: https://github.com/travis-ci/dpl
diff --git a/vendor/gitlab-ci-yml/Terraform.gitlab-ci.yml b/vendor/gitlab-ci-yml/Terraform.gitlab-ci.yml
new file mode 100644
index 00000000000..7160fce26a8
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Terraform.gitlab-ci.yml
@@ -0,0 +1,55 @@
+# Official image for Hashicorp's Terraform. It uses light image which is Alpine
+# based as it is much lighter.
+#
+# Entrypoint is also needed as image by default set `terraform` binary as an
+# entrypoint.
+image:
+ name: hashicorp/terraform:light
+ entrypoint:
+ - '/usr/bin/env'
+ - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
+
+# Default output file for Terraform plan
+variables:
+ PLAN: plan.tfplan
+
+cache:
+ paths:
+ - .terraform
+
+before_script:
+ - terraform --version
+ - terraform init
+
+stages:
+ - validate
+ - build
+ - deploy
+
+validate:
+ stage: validate
+ script:
+ - terraform validate
+
+plan:
+ stage: build
+ script:
+ - terraform plan -out=$PLAN
+ artifacts:
+ name: plan
+ paths:
+ - $PLAN
+
+# Separate apply job for manual launching Terraform as it can be destructive
+# action.
+apply:
+ stage: deploy
+ environment:
+ name: production
+ script:
+ - terraform apply -input=false $PLAN
+ dependencies:
+ - plan
+ when: manual
+ only:
+ - master
diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz
new file mode 100644
index 00000000000..69e35e6aa40
--- /dev/null
+++ b/vendor/project_templates/express.tar.gz
Binary files differ
diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz
index b54cae3143a..561b1e5902c 100644
--- a/vendor/project_templates/rails.tar.gz
+++ b/vendor/project_templates/rails.tar.gz
Binary files differ
diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz
new file mode 100644
index 00000000000..0ba6ec7c60c
--- /dev/null
+++ b/vendor/project_templates/spring.tar.gz
Binary files differ
diff --git a/yarn.lock b/yarn.lock
index c9e1b630a9e..de4a9ac4487 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -14,8 +14,8 @@ accepts@1.3.3, accepts@~1.3.3:
negotiator "0.6.1"
acorn-dynamic-import@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.1.tgz#23f671eb6e650dab277fef477c321b1178a8cca2"
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4"
dependencies:
acorn "^4.0.3"
@@ -25,17 +25,17 @@ acorn-jsx@^3.0.0:
dependencies:
acorn "^3.0.4"
-acorn@4.0.4, acorn@^4.0.3:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a"
-
acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
-acorn@^5.0.0, acorn@^5.0.3:
- version "5.0.3"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.0.3.tgz#c460df08491463f028ccb82eab3730bf01087b3d"
+acorn@^4.0.3:
+ version "4.0.13"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
+
+acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75"
after@0.8.2:
version "0.8.2"
@@ -56,6 +56,13 @@ ajv@^4.7.0:
co "^4.6.0"
json-stable-stringify "^1.0.1"
+ajv@^4.9.1:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
ajv@^5.1.5:
version "5.2.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.0.tgz#c1735024c5da2ef75cc190713073d44f098bf486"
@@ -102,11 +109,11 @@ ansi-styles@^2.2.1:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
anymatch@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507"
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
dependencies:
- arrify "^1.0.0"
micromatch "^2.1.5"
+ normalize-path "^2.0.0"
append-transform@^0.4.0:
version "0.4.0"
@@ -115,15 +122,15 @@ append-transform@^0.4.0:
default-require-extensions "^1.0.0"
aproba@^1.0.3:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.0.tgz#4d8f047a318604e18e3c06a0e52230d3d19f147b"
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.1.2.tgz#45c6629094de4e96f693ef7eab74ae079c240fc1"
are-we-there-yet@~1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.2.tgz#80e470e95a084794fe1899262c5667c6e88de1b3"
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
dependencies:
delegates "^1.0.0"
- readable-stream "^2.0.0 || ^1.1.13"
+ readable-stream "^2.0.6"
argparse@^1.0.7:
version "1.0.9"
@@ -195,14 +202,14 @@ asn1@~0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
assert-plus@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
-assert-plus@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
-
assert@^1.1.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
@@ -213,17 +220,19 @@ async-each@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
-async@0.2.x:
- version "0.2.10"
- resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
-
async@1.x, async@^1.4.0, async@^1.4.2, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+async@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
+ dependencies:
+ lodash "^4.14.0"
+
async@^2.1.2, async@^2.1.4:
- version "2.1.4"
- resolved "https://registry.yarnpkg.com/async/-/async-2.1.4.tgz#2d2160c7788032e4dd6cbe2502f1f9a2c8f6cde4"
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d"
dependencies:
lodash "^4.14.0"
@@ -261,7 +270,7 @@ axios@^0.16.2:
follow-redirects "^1.2.3"
is-buffer "^1.1.5"
-babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
+babel-code-frame@^6.11.0, babel-code-frame@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
dependencies:
@@ -269,6 +278,14 @@ babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
esutils "^2.0.2"
js-tokens "^3.0.0"
+babel-code-frame@^6.16.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+ dependencies:
+ chalk "^1.1.3"
+ esutils "^2.0.2"
+ js-tokens "^3.0.2"
+
babel-core@^6.22.1, babel-core@^6.23.0:
version "6.23.1"
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.23.1.tgz#c143cb621bb2f621710c220c5d579d15b8a442df"
@@ -864,6 +881,10 @@ balanced-match@^0.4.1, balanced-match@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
base64-arraybuffer@0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
@@ -876,9 +897,9 @@ base64id@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
-batch@0.5.3:
- version "0.5.3"
- resolved "https://registry.yarnpkg.com/batch/-/batch-0.5.3.tgz#3f3414f380321743bfc1042f9a83ff1d5824d464"
+batch@0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
bcrypt-pbkdf@^1.0.0:
version "1.0.1"
@@ -897,8 +918,8 @@ big.js@^3.1.3:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.1.3.tgz#4cada2193652eb3ca9ec8e55c9015669c9806978"
binary-extensions@^1.0.0:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.8.0.tgz#48ec8d16df4377eae5fa5884682480af4d95c774"
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0"
blob@0.0.4:
version "0.0.4"
@@ -914,10 +935,14 @@ bluebird@^2.10.2:
version "2.11.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
-bluebird@^3.0.5, bluebird@^3.1.1, bluebird@^3.3.0:
+bluebird@^3.0.5, bluebird@^3.1.1:
version "3.4.7"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
+bluebird@^3.3.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
+
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.6"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.6.tgz#53344adb14617a13f6e8dd2ce28905d1c0ba3215"
@@ -965,6 +990,13 @@ brace-expansion@^1.0.0:
balanced-match "^0.4.1"
concat-map "0.0.1"
+brace-expansion@^1.1.8:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
braces@^0.1.2:
version "0.1.5"
resolved "https://registry.yarnpkg.com/braces/-/braces-0.1.5.tgz#c085711085291d8b75fdd74eab0f8597280711e6"
@@ -1069,14 +1101,14 @@ builtin-status-codes@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
-bytes@2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070"
-
bytes@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339"
+bytes@2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.5.0.tgz#4c9423ea2d252c270c41b2bdefeff9bb6b62c06a"
+
caller-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@@ -1127,9 +1159,9 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
version "1.0.30000649"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000649.tgz#1ee1754a6df235450c8b7cd15e0ebf507221a86a"
-caseless@~0.11.0:
- version "0.11.0"
- resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
center-align@^0.1.1:
version "0.1.3"
@@ -1148,22 +1180,7 @@ chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
-chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.0:
- version "1.6.1"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2"
- dependencies:
- anymatch "^1.3.0"
- async-each "^1.0.0"
- glob-parent "^2.0.0"
- inherits "^2.0.1"
- is-binary-path "^1.0.0"
- is-glob "^2.0.0"
- path-is-absolute "^1.0.0"
- readdirp "^2.0.0"
- optionalDependencies:
- fsevents "^1.0.0"
-
-chokidar@^1.7.0:
+chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.0, chokidar@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
dependencies:
@@ -1185,8 +1202,8 @@ cipher-base@^1.0.0, cipher-base@^1.0.1:
inherits "^2.0.1"
circular-json@^0.3.1:
- version "0.3.1"
- resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d"
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
clap@^1.0.9:
version "1.1.3"
@@ -1294,7 +1311,7 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
dependencies:
delayed-stream "~1.0.0"
-commander@^2.8.1, commander@^2.9.0:
+commander@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
dependencies:
@@ -1320,37 +1337,36 @@ component-inherit@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
-compressible@~2.0.8:
- version "2.0.9"
- resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.9.tgz#6daab4e2b599c2770dd9e21e7a891b1c5a755425"
+compressible@~2.0.10:
+ version "2.0.11"
+ resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.11.tgz#16718a75de283ed8e604041625a2064586797d8a"
dependencies:
- mime-db ">= 1.24.0 < 2"
+ mime-db ">= 1.29.0 < 2"
-compression-webpack-plugin@^0.3.2:
- version "0.3.2"
- resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-0.3.2.tgz#1edfb0e749d7366d3e701670c463359b2c0cf704"
+compression-webpack-plugin@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/compression-webpack-plugin/-/compression-webpack-plugin-1.0.0.tgz#5c5eb6afd08ca6a5d66006eeef71da17b01bd676"
dependencies:
- async "0.2.x"
- webpack-sources "^0.1.0"
- optionalDependencies:
- node-zopfli "^2.0.0"
+ async "2.4.1"
+ webpack-sources "^1.0.1"
compression@^1.5.2:
- version "1.6.2"
- resolved "https://registry.yarnpkg.com/compression/-/compression-1.6.2.tgz#cceb121ecc9d09c52d7ad0c3350ea93ddd402bc3"
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.0.tgz#030c9f198f1643a057d776a738e922da4373012d"
dependencies:
accepts "~1.3.3"
- bytes "2.3.0"
- compressible "~2.0.8"
- debug "~2.2.0"
+ bytes "2.5.0"
+ compressible "~2.0.10"
+ debug "2.6.8"
on-headers "~1.0.1"
- vary "~1.1.0"
+ safe-buffer "5.1.1"
+ vary "~1.1.1"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
-concat-stream@^1.4.6:
+concat-stream@^1.5.2:
version "1.6.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
dependencies:
@@ -1383,11 +1399,11 @@ connect-history-api-fallback@^1.3.0:
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169"
connect@^3.6.0:
- version "3.6.2"
- resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.2.tgz#694e8d20681bfe490282c8ab886be98f09f42fe7"
+ version "3.6.3"
+ resolved "https://registry.yarnpkg.com/connect/-/connect-3.6.3.tgz#f7320d46a25b4be7b483a2236517f24b1e27e301"
dependencies:
- debug "2.6.7"
- finalhandler "1.0.3"
+ debug "2.6.8"
+ finalhandler "1.0.4"
parseurl "~1.3.1"
utils-merge "1.0.0"
@@ -1448,7 +1464,11 @@ copy-webpack-plugin@^4.0.1:
minimatch "^3.0.0"
node-dir "^0.1.10"
-core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1:
+core-js@^2.2.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.0.tgz#569c050918be6486b3837552028ae0466b717086"
+
+core-js@^2.4.0, core-js@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
@@ -1456,7 +1476,7 @@ core-js@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65"
-core-util-is@~1.0.0:
+core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -1654,7 +1674,7 @@ de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
-debug@2.2.0, debug@~2.2.0:
+debug@2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
dependencies:
@@ -1672,7 +1692,7 @@ debug@2.6.7:
dependencies:
ms "2.0.0"
-debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.4.5, debug@^2.6.6, debug@^2.6.8:
+debug@2.6.8, debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.4.5, debug@^2.6.6, debug@^2.6.8:
version "2.6.8"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
@@ -1697,8 +1717,8 @@ deep-equal@^1.0.1:
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
deep-extend@~0.4.0:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253"
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
deep-is@~0.1.3:
version "0.1.3"
@@ -1710,12 +1730,6 @@ default-require-extensions@^1.0.0:
dependencies:
strip-bom "^2.0.0"
-defaults@^1.0.2:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
- dependencies:
- clone "^1.0.2"
-
defined@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
@@ -1755,10 +1769,14 @@ delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
-depd@1.1.0, depd@~1.1.0:
+depd@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3"
+depd@1.1.1, depd@~1.1.0, depd@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
+
des.js@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
@@ -1776,6 +1794,10 @@ detect-indent@^4.0.0:
dependencies:
repeating "^2.0.0"
+detect-node@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.3.tgz#a2033c09cc8e158d37748fbde7507832bd6ce127"
+
di@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
@@ -1793,8 +1815,8 @@ dns-equal@^1.0.0:
resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
dns-packet@^1.0.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.1.1.tgz#2369d45038af045f3898e6fa56862aed3f40296c"
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.2.2.tgz#a8a26bec7646438963fc86e06f8f8b16d6c8bf7a"
dependencies:
ip "^1.1.0"
safe-buffer "^5.0.1"
@@ -1805,13 +1827,20 @@ dns-txt@^2.0.2:
dependencies:
buffer-indexof "^1.0.0"
-doctrine@1.5.0, doctrine@^1.2.2:
+doctrine@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
dependencies:
esutils "^2.0.2"
isarray "^1.0.0"
+doctrine@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.0.tgz#c73d8d2909d22291e1a007a395804da8b665fe63"
+ dependencies:
+ esutils "^2.0.2"
+ isarray "^1.0.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"
@@ -1870,10 +1899,10 @@ duplexer@^0.1.1, duplexer@~0.1.1:
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
duplexify@^3.2.0:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.0.tgz#1aa773002e1578457e9d9d4a50b0ccaaebcbd604"
+ version "3.5.1"
+ resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.1.tgz#4e1516be68838bc90a49994f0b39a6e5960befcd"
dependencies:
- end-of-stream "1.0.0"
+ end-of-stream "^1.0.0"
inherits "^2.0.1"
readable-stream "^2.0.0"
stream-shift "^1.0.0"
@@ -1926,11 +1955,11 @@ encodeurl@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20"
-end-of-stream@1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.0.0.tgz#d4596e702734a93e40e9af864319eabd99ff2f0e"
+end-of-stream@^1.0.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.0.tgz#7a90d833efda6cfa6eac0f4949dbb0fad3a63206"
dependencies:
- once "~1.3.0"
+ once "^1.4.0"
engine.io-client@1.8.3:
version "1.8.3"
@@ -2034,7 +2063,11 @@ es6-map@^0.1.3:
es6-symbol "~3.1.1"
event-emitter "~0.3.5"
-es6-promise@^3.0.2, es6-promise@~3.0.2:
+es6-promise@^3.0.2:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
+
+es6-promise@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6"
@@ -2165,16 +2198,17 @@ eslint-plugin-promise@^3.5.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.5.0.tgz#78fbb6ffe047201627569e85a6c5373af2a68fca"
eslint@^3.10.1:
- version "3.15.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.15.0.tgz#bdcc6a6c5ffe08160e7b93c066695362a91e30f2"
+ version "3.19.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-3.19.0.tgz#c8fc6201c7f40dd08941b87c085767386a679acc"
dependencies:
babel-code-frame "^6.16.0"
chalk "^1.1.3"
- concat-stream "^1.4.6"
+ concat-stream "^1.5.2"
debug "^2.1.1"
- doctrine "^1.2.2"
+ doctrine "^2.0.0"
escope "^3.6.0"
espree "^3.4.0"
+ esquery "^1.0.0"
estraverse "^4.2.0"
esutils "^2.0.2"
file-entry-cache "^2.0.0"
@@ -2204,10 +2238,10 @@ eslint@^3.10.1:
user-home "^2.0.0"
espree@^3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/espree/-/espree-3.4.0.tgz#41656fa5628e042878025ef467e78f125cb86e1d"
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.0.tgz#98358625bdd055861ea27e2867ea729faf463d8d"
dependencies:
- acorn "4.0.4"
+ acorn "^5.1.1"
acorn-jsx "^3.0.0"
esprima@2.7.x, esprima@^2.6.0, esprima@^2.7.1:
@@ -2218,6 +2252,16 @@ esprima@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+esprima@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+
+esquery@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+ dependencies:
+ estraverse "^4.0.0"
+
esrecurse@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220"
@@ -2229,7 +2273,7 @@ estraverse@^1.9.1:
version "1.9.3"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44"
-estraverse@^4.1.1, estraverse@^4.2.0:
+estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
@@ -2339,8 +2383,8 @@ exports-loader@^0.6.4:
source-map "0.5.x"
express@^4.13.3, express@^4.15.2:
- version "4.15.3"
- resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662"
+ version "4.15.4"
+ resolved "https://registry.yarnpkg.com/express/-/express-4.15.4.tgz#032e2253489cf8fce02666beca3d11ed7a2daed1"
dependencies:
accepts "~1.3.3"
array-flatten "1.1.1"
@@ -2348,23 +2392,23 @@ express@^4.13.3, express@^4.15.2:
content-type "~1.0.2"
cookie "0.3.1"
cookie-signature "1.0.6"
- debug "2.6.7"
- depd "~1.1.0"
+ debug "2.6.8"
+ depd "~1.1.1"
encodeurl "~1.0.1"
escape-html "~1.0.3"
etag "~1.8.0"
- finalhandler "~1.0.3"
+ finalhandler "~1.0.4"
fresh "0.5.0"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.1"
path-to-regexp "0.1.7"
- proxy-addr "~1.1.4"
- qs "6.4.0"
+ proxy-addr "~1.1.5"
+ qs "6.5.0"
range-parser "~1.2.0"
- send "0.15.3"
- serve-static "1.12.3"
+ send "0.15.4"
+ serve-static "1.12.4"
setprototypeof "1.0.3"
statuses "~1.3.1"
type-is "~1.6.15"
@@ -2372,8 +2416,8 @@ express@^4.13.3, express@^4.15.2:
vary "~1.1.1"
extend@^3.0.0, extend@~3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4"
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
extglob@^0.3.1:
version "0.3.2"
@@ -2381,9 +2425,9 @@ extglob@^0.3.1:
dependencies:
is-extglob "^1.0.0"
-extsprintf@1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550"
+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"
@@ -2464,11 +2508,11 @@ fill-range@^2.1.0:
repeat-element "^1.1.2"
repeat-string "^1.5.2"
-finalhandler@1.0.3, finalhandler@~1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89"
+finalhandler@1.0.4, finalhandler@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.4.tgz#18574f2e7c4b98b8ae3b230c21f201f31bdb3fb7"
dependencies:
- debug "2.6.7"
+ debug "2.6.8"
encodeurl "~1.0.1"
escape-html "~1.0.3"
on-finished "~2.3.0"
@@ -2535,8 +2579,8 @@ forever-agent@~0.6.1:
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
form-data@~2.1.1:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.2.tgz#89c3534008b97eada4cbb157d58f6f5df025eae4"
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.5"
@@ -2575,13 +2619,13 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
fsevents@^1.0.0:
- version "1.0.17"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.0.17.tgz#8537f3f12272678765b4fd6528c0f1f66f8f4558"
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.2.tgz#3282b713fb3ad80ede0e9fcf4611b5aa6fc033f4"
dependencies:
nan "^2.3.0"
- node-pre-gyp "^0.6.29"
+ node-pre-gyp "^0.6.36"
-fstream-ignore@~1.0.5:
+fstream-ignore@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
dependencies:
@@ -2589,9 +2633,9 @@ fstream-ignore@~1.0.5:
inherits "2"
minimatch "^3.0.0"
-fstream@^1.0.0, fstream@^1.0.2, fstream@~1.0.10:
- version "1.0.10"
- resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.10.tgz#604e8a92fe26ffd9f6fae30399d4984e1ab22822"
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
dependencies:
graceful-fs "^4.1.2"
inherits "~2.0.0"
@@ -2602,9 +2646,9 @@ function-bind@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
-gauge@~2.7.1:
- version "2.7.2"
- resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.2.tgz#15cecc31b02d05345a5d6b0e171cdb3ad2307774"
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
dependencies:
aproba "^1.0.3"
console-control-strings "^1.0.0"
@@ -2613,7 +2657,6 @@ gauge@~2.7.1:
signal-exit "^3.0.0"
string-width "^1.0.1"
strip-ansi "^3.0.1"
- supports-color "^0.2.0"
wide-align "^1.1.0"
generate-function@^2.0.0:
@@ -2639,8 +2682,8 @@ get-stream@^3.0.0:
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
getpass@^0.1.1:
- version "0.1.6"
- resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6"
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
dependencies:
assert-plus "^1.0.0"
@@ -2677,7 +2720,18 @@ glob@^6.0.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
+glob@^7.0.0, glob@^7.1.1:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.0.3, glob@^7.0.5:
version "7.1.1"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
dependencies:
@@ -2688,10 +2742,14 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
once "^1.3.0"
path-is-absolute "^1.0.0"
-globals@^9.0.0, globals@^9.14.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:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
globby@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
@@ -2767,7 +2825,7 @@ gzip-size@3.0.0, gzip-size@^3.0.0:
dependencies:
duplexer "^0.1.1"
-handle-thing@^1.2.4:
+handle-thing@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
@@ -2781,14 +2839,16 @@ handlebars@^4.0.1, handlebars@^4.0.3:
optionalDependencies:
uglify-js "^2.6"
-har-validator@~2.0.6:
- version "2.0.6"
- resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
+har-schema@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+
+har-validator@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
dependencies:
- chalk "^1.1.1"
- commander "^2.9.0"
- is-my-json-valid "^2.12.4"
- pinkie-promise "^2.0.0"
+ ajv "^4.9.1"
+ har-schema "^1.0.5"
has-ansi@^2.0.0:
version "2.0.0"
@@ -2885,10 +2945,14 @@ html-comment-regex@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
-html-entities@1.2.0, html-entities@^1.2.0:
+html-entities@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.0.tgz#41948caf85ce82fed36e4e6a0ed371a6664379e2"
+html-entities@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f"
+
htmlparser2@^3.8.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
@@ -2900,18 +2964,10 @@ htmlparser2@^3.8.2:
inherits "^2.0.1"
readable-stream "^2.0.2"
-http-deceiver@^1.2.4:
+http-deceiver@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
-http-errors@~1.5.0:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.5.1.tgz#788c0d2c1de2c81b9e6e8c01843b6b97eb920750"
- dependencies:
- inherits "2.0.3"
- setprototypeof "1.0.2"
- statuses ">= 1.3.1 < 2"
-
http-errors@~1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257"
@@ -2921,6 +2977,15 @@ http-errors@~1.6.1:
setprototypeof "1.0.3"
statuses ">= 1.3.1 < 2"
+http-errors@~1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
+ dependencies:
+ depd "1.1.1"
+ inherits "2.0.3"
+ setprototypeof "1.0.3"
+ statuses ">= 1.3.1 < 2"
+
http-proxy-middleware@~0.17.4:
version "0.17.4"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833"
@@ -2966,8 +3031,8 @@ ignore-by-default@^1.0.0:
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
ignore@^3.2.0:
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.2.tgz#1c51e1ef53bab6ddc15db4d9ac4ec139eceb3410"
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
immediate@~3.0.5:
version "3.0.6"
@@ -3009,7 +3074,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1:
+inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@@ -3059,13 +3124,13 @@ invert-kv@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
-ip@^1.1.0:
+ip@^1.1.0, ip@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
-ipaddr.js@1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec"
+ipaddr.js@1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0"
is-absolute-url@^2.0.0:
version "2.1.0"
@@ -3148,9 +3213,9 @@ is-glob@^3.1.0:
dependencies:
is-extglob "^2.1.0"
-is-my-json-valid@^2.10.0, is-my-json-valid@^2.12.4:
- version "2.15.0"
- resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b"
+is-my-json-valid@^2.10.0:
+ version "2.16.0"
+ resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz#f079dd9bfdae65ee2038aae8acbc86ab109e3693"
dependencies:
generate-function "^2.0.0"
generate-object-property "^1.1.0"
@@ -3271,6 +3336,10 @@ isexe@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-1.1.2.tgz#36f3e22e60750920f5e7241a476a8c6a42275ad0"
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
isobject@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
@@ -3383,12 +3452,6 @@ jed@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4"
-jodid25519@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967"
- dependencies:
- jsbn "~0.1.0"
-
jquery-ujs@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/jquery-ujs/-/jquery-ujs-1.2.1.tgz#6ee75b1ef4e9ac95e7124f8d71f7d351f5548e92"
@@ -3420,13 +3483,24 @@ js-tokens@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
-js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0:
+js-tokens@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@3.x, js-yaml@^3.4.3, js-yaml@^3.7.0:
version "3.8.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628"
dependencies:
argparse "^1.0.7"
esprima "^3.1.1"
+js-yaml@^3.5.1:
+ version "3.9.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
js-yaml@~3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.7.0.tgz#5c967ddd837a9bfdca5f2de84253abe8a1c03b80"
@@ -3435,8 +3509,8 @@ js-yaml@~3.7.0:
esprima "^2.6.0"
jsbn@~0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd"
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
jsesc@^1.3.0:
version "1.3.0"
@@ -3447,12 +3521,12 @@ jsesc@~0.5.0:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
json-loader@^0.5.4:
- version "0.5.4"
- resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de"
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d"
json-schema-traverse@^0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.0.tgz#0016c0b1ca1efe46d44d37541bcdfc19dcfae0db"
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
json-schema@0.2.3:
version "0.2.3"
@@ -3491,12 +3565,13 @@ jsonpointer@^4.0.0:
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
jsprim@^1.2.2:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252"
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
dependencies:
- extsprintf "1.0.2"
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
json-schema "0.2.3"
- verror "1.3.6"
+ verror "1.10.0"
jszip-utils@^0.0.2:
version "0.0.2"
@@ -3960,11 +4035,21 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-"mime-db@>= 1.24.0 < 2", mime-db@~1.27.0:
+"mime-db@>= 1.29.0 < 2", mime-db@~1.29.0:
+ version "1.29.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.29.0.tgz#48d26d235589651704ac5916ca06001914266878"
+
+mime-db@~1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
-mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7:
+mime-types@^2.1.12, mime-types@~2.1.7:
+ version "2.1.16"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.16.tgz#2b858a52e5ecd516db897ac2be87487830698e23"
+ dependencies:
+ mime-db "~1.29.0"
+
+mime-types@~2.1.11, mime-types@~2.1.15:
version "2.1.15"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
dependencies:
@@ -3992,6 +4077,12 @@ minimalistic-assert@^1.0.0:
dependencies:
brace-expansion "^1.0.0"
+minimatch@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.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"
@@ -4049,9 +4140,9 @@ name-all-modules-plugin@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/name-all-modules-plugin/-/name-all-modules-plugin-1.0.1.tgz#0abfb6ad835718b9fb4def0674e06657a954375c"
-nan@^2.0.0, nan@^2.3.0:
- version "2.5.1"
- resolved "https://registry.yarnpkg.com/nan/-/nan-2.5.1.tgz#d5b01691253326a97a2bbee9e61c55d8d60351e2"
+nan@^2.3.0:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
natural-compare@^1.4.0:
version "1.4.0"
@@ -4133,28 +4224,19 @@ node-libs-browser@^2.0.0:
util "^0.10.3"
vm-browserify "0.0.4"
-node-pre-gyp@^0.6.29, node-pre-gyp@^0.6.4:
- version "0.6.33"
- resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.33.tgz#640ac55198f6a925972e0c16c4ac26a034d5ecc9"
+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"
dependencies:
- mkdirp "~0.5.1"
- nopt "~3.0.6"
- npmlog "^4.0.1"
- rc "~1.1.6"
- request "^2.79.0"
- rimraf "~2.5.4"
- semver "~5.3.0"
- tar "~2.2.1"
- tar-pack "~3.3.0"
-
-node-zopfli@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/node-zopfli/-/node-zopfli-2.0.2.tgz#a7a473ae92aaea85d4c68d45bbf2c944c46116b8"
- dependencies:
- commander "^2.8.1"
- defaults "^1.0.2"
- nan "^2.0.0"
- node-pre-gyp "^0.6.4"
+ mkdirp "^0.5.1"
+ nopt "^4.0.1"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ request "^2.81.0"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^2.2.1"
+ tar-pack "^3.4.0"
nodemon@^1.11.0:
version "1.11.0"
@@ -4171,19 +4253,26 @@ nodemon@^1.11.0:
undefsafe "0.0.3"
update-notifier "0.5.0"
-nopt@3.x, nopt@~3.0.1, nopt@~3.0.6:
+nopt@3.x, nopt@~3.0.1:
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
dependencies:
abbrev "1"
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
nopt@~1.0.10:
version "1.0.10"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
dependencies:
abbrev "1"
-normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
+normalize-package-data@^2.3.2:
version "2.3.5"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df"
dependencies:
@@ -4192,9 +4281,20 @@ normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"
-normalize-path@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.0.1.tgz#47886ac1662760d4261b7d979d241709d3ce3f7a"
+normalize-package-data@^2.3.4:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^2.0.0, normalize-path@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
normalize-range@^0.1.2:
version "0.1.2"
@@ -4215,13 +4315,13 @@ npm-run-path@^2.0.0:
dependencies:
path-key "^2.0.0"
-npmlog@^4.0.1:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f"
+npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
dependencies:
are-we-there-yet "~1.1.2"
console-control-strings "~1.1.0"
- gauge "~2.7.1"
+ gauge "~2.7.3"
set-blocking "~2.0.0"
null-check@^1.0.0:
@@ -4263,7 +4363,7 @@ object.omit@^2.0.0:
for-own "^0.1.4"
is-extendable "^0.1.1"
-obuf@^1.0.0, obuf@^1.1.0:
+obuf@^1.0.0, obuf@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.1.tgz#104124b6c602c6796881a042541d36db43a5264e"
@@ -4277,18 +4377,12 @@ on-headers@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
-once@1.x, once@^1.3.0, once@^1.4.0:
+once@1.x, once@^1.3.0, once@^1.3.3, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies:
wrappy "1"
-once@~1.3.0, once@~1.3.3:
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20"
- dependencies:
- wrappy "1"
-
onetime@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
@@ -4354,11 +4448,11 @@ os-locale@^2.0.0:
lcid "^1.0.0"
mem "^1.1.0"
-os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
-osenv@^0.1.0:
+osenv@^0.1.0, osenv@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
dependencies:
@@ -4515,6 +4609,10 @@ pbkdf2@^3.0.3:
dependencies:
create-hmac "^1.1.2"
+performance-now@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+
pify@^2.0.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -4868,12 +4966,12 @@ proto-list@~1.2.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
-proxy-addr@~1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3"
+proxy-addr@~1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.5.tgz#71c0ee3b102de3f202f3b64f608d173fcba1a918"
dependencies:
forwarded "~0.1.0"
- ipaddr.js "1.3.0"
+ ipaddr.js "1.4.0"
prr@~0.0.0:
version "0.0.0"
@@ -4915,13 +5013,13 @@ qjobs@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73"
-qs@6.4.0:
+qs@6.4.0, qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
-qs@~6.3.0:
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
+qs@6.5.0:
+ version "6.5.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.0.tgz#8d04954d364def3efc55b5a0793e1e2c8b1e6e49"
query-string@^4.1.0:
version "4.3.2"
@@ -4985,14 +5083,14 @@ raw-loader@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa"
-rc@^1.0.1, rc@~1.1.6:
- version "1.1.6"
- resolved "https://registry.yarnpkg.com/rc/-/rc-1.1.6.tgz#43651b76b6ae53b5c802f1151fa3fc3b059969c9"
+rc@^1.0.1, rc@^1.1.7:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
dependencies:
deep-extend "~0.4.0"
ini "~1.3.0"
minimist "^1.2.0"
- strip-json-comments "~1.0.4"
+ strip-json-comments "~2.0.1"
react-dev-utils@^0.5.2:
version "0.5.2"
@@ -5046,16 +5144,16 @@ read-pkg@^2.0.0:
normalize-package-data "^2.3.2"
path-type "^2.0.0"
-readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.1.0, readable-stream@^2.2.2:
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e"
+readable-stream@^2.0.0, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.9:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
dependencies:
- buffer-shims "^1.0.0"
core-util-is "~1.0.0"
- inherits "~2.0.1"
+ inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~1.0.6"
- string_decoder "~0.10.x"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.0.3"
util-deprecate "~1.0.1"
readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@~2.0.6:
@@ -5069,26 +5167,26 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable
string_decoder "~0.10.x"
util-deprecate "~1.0.1"
-readable-stream@~1.0.2:
- version "1.0.34"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
+readable-stream@^2.1.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e"
dependencies:
+ buffer-shims "^1.0.0"
core-util-is "~1.0.0"
inherits "~2.0.1"
- isarray "0.0.1"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
string_decoder "~0.10.x"
+ util-deprecate "~1.0.1"
-readable-stream@~2.1.4:
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.1.5.tgz#66fa8b720e1438b364681f2ad1a63c618448c9d0"
+readable-stream@~1.0.2:
+ version "1.0.34"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
dependencies:
- buffer-shims "^1.0.0"
core-util-is "~1.0.0"
inherits "~2.0.1"
- isarray "~1.0.0"
- process-nextick-args "~1.0.6"
+ isarray "0.0.1"
string_decoder "~0.10.x"
- util-deprecate "~1.0.1"
readdirp@^2.0.0:
version "2.1.0"
@@ -5195,6 +5293,10 @@ regjsparser@^0.1.4:
dependencies:
jsesc "~0.5.0"
+remove-trailing-separator@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+
repeat-element@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
@@ -5219,18 +5321,18 @@ repeating@^2.0.0:
dependencies:
is-finite "^1.0.0"
-request@^2.79.0:
- version "2.79.0"
- resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
+request@^2.81.0:
+ version "2.81.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
dependencies:
aws-sign2 "~0.6.0"
aws4 "^1.2.1"
- caseless "~0.11.0"
+ caseless "~0.12.0"
combined-stream "~1.0.5"
extend "~3.0.0"
forever-agent "~0.6.1"
form-data "~2.1.1"
- har-validator "~2.0.6"
+ har-validator "~4.2.1"
hawk "~3.1.3"
http-signature "~1.1.0"
is-typedarray "~1.0.0"
@@ -5238,10 +5340,12 @@ request@^2.79.0:
json-stringify-safe "~5.0.1"
mime-types "~2.1.7"
oauth-sign "~0.8.1"
- qs "~6.3.0"
+ performance-now "^0.2.0"
+ qs "~6.4.0"
+ safe-buffer "^5.0.1"
stringstream "~0.0.4"
tough-cookie "~2.3.0"
- tunnel-agent "~0.4.1"
+ tunnel-agent "^0.6.0"
uuid "^3.0.0"
require-directory@^2.1.1:
@@ -5292,18 +5396,12 @@ right-align@^0.1.1:
dependencies:
align-text "^0.1.1"
-rimraf@2, rimraf@^2.2.8, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@^2.6.0:
+rimraf@2, rimraf@^2.2.8, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.6.0, rimraf@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
dependencies:
glob "^7.0.5"
-rimraf@~2.5.1, rimraf@~2.5.4:
- version "2.5.4"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04"
- dependencies:
- glob "^7.0.5"
-
ripemd160@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-1.0.1.tgz#93a4bbd4942bc574b69a8fa57c71de10ecca7d6e"
@@ -5318,6 +5416,10 @@ rx-lite@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
+safe-buffer@5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
safe-buffer@^5.0.1, safe-buffer@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
@@ -5339,8 +5441,8 @@ select@^1.1.2:
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
selfsigned@^1.9.1:
- version "1.9.1"
- resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.9.1.tgz#cdda4492d70d486570f87c65546023558e1dfa5a"
+ version "1.10.1"
+ resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.1.tgz#bf8cb7b83256c4551e31347c6311778db99eec52"
dependencies:
node-forge "0.6.33"
@@ -5350,26 +5452,30 @@ semver-diff@^2.0.0:
dependencies:
semver "^5.0.3"
-"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.3.0, semver@~5.3.0:
+"semver@2 || 3 || 4 || 5", semver@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+semver@^5.0.3:
+ version "5.4.1"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
+
semver@~4.3.3:
version "4.3.6"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
-send@0.15.3:
- version "0.15.3"
- resolved "https://registry.yarnpkg.com/send/-/send-0.15.3.tgz#5013f9f99023df50d1bd9892c19e3defd1d53309"
+send@0.15.4:
+ version "0.15.4"
+ resolved "https://registry.yarnpkg.com/send/-/send-0.15.4.tgz#985faa3e284b0273c793364a35c6737bd93905b9"
dependencies:
- debug "2.6.7"
- depd "~1.1.0"
+ debug "2.6.8"
+ depd "~1.1.1"
destroy "~1.0.4"
encodeurl "~1.0.1"
escape-html "~1.0.3"
etag "~1.8.0"
fresh "0.5.0"
- http-errors "~1.6.1"
+ http-errors "~1.6.2"
mime "1.3.4"
ms "2.0.0"
on-finished "~2.3.0"
@@ -5377,25 +5483,25 @@ send@0.15.3:
statuses "~1.3.1"
serve-index@^1.7.2:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.8.0.tgz#7c5d96c13fb131101f93c1c5774f8516a1e78d3b"
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.0.tgz#d2b280fc560d616ee81b48bf0fa82abed2485ce7"
dependencies:
accepts "~1.3.3"
- batch "0.5.3"
- debug "~2.2.0"
+ batch "0.6.1"
+ debug "2.6.8"
escape-html "~1.0.3"
- http-errors "~1.5.0"
- mime-types "~2.1.11"
+ http-errors "~1.6.1"
+ mime-types "~2.1.15"
parseurl "~1.3.1"
-serve-static@1.12.3:
- version "1.12.3"
- resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2"
+serve-static@1.12.4:
+ version "1.12.4"
+ resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.4.tgz#9b6aa98eeb7253c4eedc4c1f6fdbca609901a961"
dependencies:
encodeurl "~1.0.1"
escape-html "~1.0.3"
parseurl "~1.3.1"
- send "0.15.3"
+ send "0.15.4"
set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
@@ -5409,10 +5515,6 @@ setimmediate@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
-setprototypeof@1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.2.tgz#81a552141ec104b88e89ce383103ad5c66564d08"
-
setprototypeof@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
@@ -5434,8 +5536,8 @@ shebang-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
shelljs@^0.7.5:
- version "0.7.6"
- resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.6.tgz#379cccfb56b91c8601e4793356eb5382924de9ad"
+ version "0.7.8"
+ resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3"
dependencies:
glob "^7.0.0"
interpret "^1.0.0"
@@ -5546,7 +5648,7 @@ sort-keys@^1.0.0:
dependencies:
is-plain-obj "^1.0.0"
-source-list-map@^0.1.7, source-list-map@~0.1.7:
+source-list-map@^0.1.7:
version "0.1.8"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106"
@@ -5596,25 +5698,28 @@ spdx-license-ids@^1.0.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
-spdy-transport@^2.0.15:
- version "2.0.18"
- resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.0.18.tgz#43fc9c56be2cccc12bb3e2754aa971154e836ea6"
+spdy-transport@^2.0.18:
+ version "2.0.20"
+ resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-2.0.20.tgz#735e72054c486b2354fe89e702256004a39ace4d"
dependencies:
- debug "^2.2.0"
+ debug "^2.6.8"
+ detect-node "^2.0.3"
hpack.js "^2.1.6"
- obuf "^1.1.0"
- readable-stream "^2.0.1"
- wbuf "^1.4.0"
+ obuf "^1.1.1"
+ readable-stream "^2.2.9"
+ safe-buffer "^5.0.1"
+ wbuf "^1.7.2"
spdy@^3.4.1:
- version "3.4.4"
- resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.4.tgz#e0406407ca90ff01b553eb013505442649f5a819"
+ version "3.4.7"
+ resolved "https://registry.yarnpkg.com/spdy/-/spdy-3.4.7.tgz#42ff41ece5cc0f99a3a6c28aabb73f5c3b03acbc"
dependencies:
- debug "^2.2.0"
- handle-thing "^1.2.4"
- http-deceiver "^1.2.4"
+ debug "^2.6.8"
+ handle-thing "^1.2.5"
+ http-deceiver "^1.2.7"
+ safe-buffer "^5.0.1"
select-hose "^2.0.0"
- spdy-transport "^2.0.15"
+ spdy-transport "^2.0.18"
split@0.3:
version "0.3.3"
@@ -5631,8 +5736,8 @@ sql.js@^0.4.0:
resolved "https://registry.yarnpkg.com/sql.js/-/sql.js-0.4.0.tgz#23be9635520eb0ff43a741e7e830397266e88445"
sshpk@^1.7.0:
- version "1.10.2"
- resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.2.tgz#d5a804ce22695515638e798dbe23273de070a5fa"
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
@@ -5641,7 +5746,6 @@ sshpk@^1.7.0:
optionalDependencies:
bcrypt-pbkdf "^1.0.0"
ecc-jsbn "~0.1.1"
- jodid25519 "^1.0.0"
jsbn "~0.1.0"
tweetnacl "~0.14.0"
@@ -5705,6 +5809,12 @@ 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"
+string_decoder@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
+ dependencies:
+ safe-buffer "~5.1.0"
+
stringstream@~0.0.4:
version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
@@ -5735,18 +5845,10 @@ strip-indent@^1.0.1:
dependencies:
get-stdin "^4.0.1"
-strip-json-comments@~1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
-
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
-supports-color@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a"
-
supports-color@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
@@ -5791,23 +5893,23 @@ tapable@^0.1.8:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4"
tapable@^0.2.7:
- version "0.2.7"
- resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.7.tgz#e46c0daacbb2b8a98b9b0cea0f4052105817ed5c"
+ version "0.2.8"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
-tar-pack@~3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.3.0.tgz#30931816418f55afc4d21775afdd6720cee45dae"
- dependencies:
- debug "~2.2.0"
- fstream "~1.0.10"
- fstream-ignore "~1.0.5"
- once "~1.3.3"
- readable-stream "~2.1.4"
- rimraf "~2.5.1"
- tar "~2.2.1"
- uid-number "~0.0.6"
-
-tar@~2.2.1:
+tar-pack@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
+ dependencies:
+ debug "^2.2.0"
+ fstream "^1.0.10"
+ fstream-ignore "^1.0.5"
+ once "^1.3.3"
+ readable-stream "^2.1.4"
+ rimraf "^2.5.1"
+ tar "^2.2.1"
+ uid-number "^0.0.6"
+
+tar@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
dependencies:
@@ -5849,6 +5951,10 @@ thunky@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e"
+time-stamp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-2.0.0.tgz#95c6a44530e15ba8d6f4a3ecb8c3a3fac46da357"
+
timeago.js@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-2.0.5.tgz#730c74fbdb0b0917a553675a4460e3a7f80db86c"
@@ -5868,8 +5974,8 @@ timers-browserify@^1.4.2:
process "~0.11.0"
timers-browserify@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.2.tgz#ab4883cf597dcd50af211349a00fbca56ac86b86"
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6"
dependencies:
setimmediate "^1.0.4"
@@ -5877,12 +5983,18 @@ tiny-emitter@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-1.1.0.tgz#ab405a21ffed814a76c19739648093d70654fecb"
-tmp@0.0.31, tmp@0.0.x:
+tmp@0.0.31:
version "0.0.31"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7"
dependencies:
os-tmpdir "~1.0.1"
+tmp@0.0.x:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ dependencies:
+ os-tmpdir "~1.0.2"
+
to-array@0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
@@ -5927,9 +6039,11 @@ tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
-tunnel-agent@~0.4.1:
- version "0.4.3"
- resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ dependencies:
+ safe-buffer "^5.0.1"
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
@@ -5973,7 +6087,7 @@ uglifyjs-webpack-plugin@^0.4.6:
uglify-js "^2.8.29"
webpack-sources "^1.0.1"
-uid-number@~0.0.6:
+uid-number@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
@@ -6079,8 +6193,8 @@ user-home@^2.0.0:
os-homedir "^1.0.0"
useragent@^2.1.12:
- version "2.1.13"
- resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.1.13.tgz#bba43e8aa24d5ceb83c2937473e102e21df74c10"
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.2.1.tgz#cf593ef4f2d175875e8bb658ea92e18a4fd06d8e"
dependencies:
lru-cache "2.2.x"
tmp "0.0.x"
@@ -6104,8 +6218,8 @@ uuid@^2.0.1, uuid@^2.0.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
uuid@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
validate-npm-package-license@^3.0.1:
version "3.0.1"
@@ -6114,7 +6228,7 @@ validate-npm-package-license@^3.0.1:
spdx-correct "~1.0.0"
spdx-expression-parse "~1.0.0"
-vary@~1.1.0, vary@~1.1.1:
+vary@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37"
@@ -6122,11 +6236,13 @@ vendors@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.1.tgz#37ad73c8ee417fb3d580e785312307d274847f22"
-verror@1.3.6:
- version "1.3.6"
- resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c"
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
dependencies:
- extsprintf "1.0.2"
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
visibilityjs@^1.2.4:
version "1.2.4"
@@ -6191,6 +6307,10 @@ vue@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.2.6.tgz#451714b394dd6d4eae7b773c40c2034a59621aed"
+vuex@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/vuex/-/vuex-2.3.1.tgz#cde8e997c1f9957719bc7dea154f9aa691d981a6"
+
watchpack@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"
@@ -6199,7 +6319,7 @@ watchpack@^1.4.0:
chokidar "^1.7.0"
graceful-fs "^4.1.2"
-wbuf@^1.1.0, wbuf@^1.4.0:
+wbuf@^1.1.0, wbuf@^1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.2.tgz#d697b99f1f59512df2751be42769c1580b5801fe"
dependencies:
@@ -6221,7 +6341,7 @@ webpack-bundle-analyzer@^2.8.2:
opener "^1.4.3"
ws "^2.3.1"
-webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.11.0:
+webpack-dev-middleware@^1.0.11:
version "1.11.0"
resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9"
dependencies:
@@ -6230,9 +6350,19 @@ webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.11.0:
path-is-absolute "^1.0.0"
range-parser "^1.0.3"
+webpack-dev-middleware@^1.11.0:
+ version "1.12.0"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.12.0.tgz#d34efefb2edda7e1d3b5dbe07289513219651709"
+ dependencies:
+ memory-fs "~0.4.1"
+ mime "^1.3.4"
+ path-is-absolute "^1.0.0"
+ range-parser "^1.0.3"
+ time-stamp "^2.0.0"
+
webpack-dev-server@^2.6.1:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.6.1.tgz#0b292a9da96daf80a65988f69f87b4166e5defe7"
+ version "2.7.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.7.1.tgz#21580f5a08cd065c71144cf6f61c345bca59a8b8"
dependencies:
ansi-html "0.0.7"
bonjour "^3.5.0"
@@ -6244,6 +6374,7 @@ webpack-dev-server@^2.6.1:
html-entities "^1.2.0"
http-proxy-middleware "~0.17.4"
internal-ip "^1.2.0"
+ ip "^1.1.5"
loglevel "^1.4.1"
opn "4.0.2"
portfinder "^1.0.9"
@@ -6257,13 +6388,6 @@ webpack-dev-server@^2.6.1:
webpack-dev-middleware "^1.11.0"
yargs "^6.0.0"
-webpack-sources@^0.1.0:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.1.4.tgz#ccc2c817e08e5fa393239412690bb481821393cd"
- dependencies:
- source-list-map "~0.1.7"
- source-map "~0.5.3"
-
webpack-sources@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
@@ -6275,9 +6399,9 @@ webpack-stats-plugin@^0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.1.5.tgz#29e5f12ebfd53158d31d656a113ac1f7b86179d9"
-webpack@^3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.4.0.tgz#e9465b660ad79dd2d33874d968b31746ea9a8e63"
+webpack@^3.5.5:
+ version "3.5.5"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.5.5.tgz#3226f09fc8b3e435ff781e7af34f82b68b26996c"
dependencies:
acorn "^5.0.0"
acorn-dynamic-import "^2.0.0"
@@ -6324,17 +6448,23 @@ which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
-which@^1.1.1, which@^1.2.1, which@^1.2.9:
+which@^1.1.1, which@^1.2.1:
version "1.2.12"
resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192"
dependencies:
isexe "^1.1.1"
+which@^1.2.9:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+ dependencies:
+ isexe "^2.0.0"
+
wide-align@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.0.tgz#40edde802a71fea1f070da3e62dcda2e7add96ad"
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
dependencies:
- string-width "^1.0.1"
+ string-width "^1.0.2"
window-size@0.1.0:
version "0.1.0"
@@ -6364,8 +6494,8 @@ wrappy@1:
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
write-file-atomic@^1.1.2:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.1.tgz#7d45ba32316328dd1ec7d90f60ebc0d845bb759a"
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f"
dependencies:
graceful-fs "^4.1.11"
imurmurhash "^0.1.4"