summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml5
-rw-r--r--.rubocop.yml22
-rw-r--r--.rubocop_todo.yml8
-rw-r--r--CHANGELOG.md18
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile21
-rw-r--r--Gemfile.lock70
-rw-r--r--README.md1
-rw-r--r--app/assets/javascripts/api.js3
-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/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.js (renamed from app/assets/javascripts/vue_shared/common_vue.js)1
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js8
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js4
-rw-r--r--app/assets/javascripts/dispatcher.js5
-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/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.js27
-rw-r--r--app/assets/javascripts/gl_dropdown.js22
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue6
-rw-r--r--app/assets/javascripts/issuable_form.js52
-rw-r--r--app/assets/javascripts/issue.js7
-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/lib/utils/common_utils.js14
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js13
-rw-r--r--app/assets/javascripts/lib/utils/sticky.js2
-rw-r--r--app/assets/javascripts/main.js7
-rw-r--r--app/assets/javascripts/merge_request_tabs.js1
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue (renamed from app/assets/javascripts/monitoring/components/monitoring.vue)56
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue (renamed from app/assets/javascripts/monitoring/components/monitoring_state.vue)52
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue (renamed from app/assets/javascripts/monitoring/components/monitoring_column.vue)154
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue (renamed from app/assets/javascripts/monitoring/components/monitoring_deployment.vue)9
-rw-r--r--app/assets/javascripts/monitoring/components/graph/flag.vue (renamed from app/assets/javascripts/monitoring/components/monitoring_flag.vue)18
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue (renamed from app/assets/javascripts/monitoring/components/monitoring_legends.vue)87
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue21
-rw-r--r--app/assets/javascripts/monitoring/components/graph_row.vue (renamed from app/assets/javascripts/monitoring/components/monitoring_row.vue)14
-rw-r--r--app/assets/javascripts/monitoring/components/monitoring_paths.vue40
-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.js78
-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.js6
-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/pipelines/components/graph/dropdown_job_component.vue2
-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/pipelines.vue4
-rw-r--r--app/assets/javascripts/project.js4
-rw-r--r--app/assets/javascripts/project_select.js42
-rw-r--r--app/assets/javascripts/project_select_combo_button.js16
-rw-r--r--app/assets/javascripts/project_visibility.js41
-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/right_sidebar.js9
-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/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/vue_merge_request_widget/components/mr_widget_header.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/identicon.vue (renamed from app/assets/javascripts/groups/components/group_identicon.vue)8
-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/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/buttons.scss17
-rw-r--r--app/assets/stylesheets/framework/calendar.scss4
-rw-r--r--app/assets/stylesheets/framework/common.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss195
-rw-r--r--app/assets/stylesheets/framework/feature_highlight.scss94
-rw-r--r--app/assets/stylesheets/framework/filters.scss18
-rw-r--r--app/assets/stylesheets/framework/header.scss13
-rw-r--r--app/assets/stylesheets/framework/selects.scss18
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss4
-rw-r--r--app/assets/stylesheets/framework/typography.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss11
-rw-r--r--app/assets/stylesheets/new_nav.scss280
-rw-r--r--app/assets/stylesheets/new_sidebar.scss10
-rw-r--r--app/assets/stylesheets/pages/builds.scss11
-rw-r--r--app/assets/stylesheets/pages/editor.scss2
-rw-r--r--app/assets/stylesheets/pages/environments.scss14
-rw-r--r--app/assets/stylesheets/pages/issuable.scss23
-rw-r--r--app/assets/stylesheets/pages/issues.scss23
-rw-r--r--app/assets/stylesheets/pages/labels.scss2
-rw-r--r--app/assets/stylesheets/pages/members.scss10
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss20
-rw-r--r--app/assets/stylesheets/pages/note_form.scss10
-rw-r--r--app/assets/stylesheets/pages/notes.scss58
-rw-r--r--app/assets/stylesheets/pages/notifications.scss4
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss223
-rw-r--r--app/assets/stylesheets/pages/projects.scss40
-rw-r--r--app/assets/stylesheets/pages/repo.scss24
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/pages/settings.scss41
-rw-r--r--app/assets/stylesheets/pages/todos.scss4
-rw-r--r--app/controllers/admin/users_controller.rb15
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/autocomplete_controller.rb24
-rw-r--r--app/controllers/concerns/issuable_collections.rb40
-rw-r--r--app/controllers/concerns/notes_actions.rb56
-rw-r--r--app/controllers/concerns/requires_whitelisted_monitoring_client.rb3
-rw-r--r--app/controllers/groups/milestones_controller.rb6
-rw-r--r--app/controllers/passwords_controller.rb10
-rw-r--r--app/controllers/profiles/passwords_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb67
-rw-r--r--app/controllers/projects/merge_requests_controller.rb9
-rw-r--r--app/controllers/projects_controller.rb5
-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/helpers/application_helper.rb6
-rw-r--r--app/helpers/application_settings_helper.rb19
-rw-r--r--app/helpers/auth_helper.rb2
-rw-r--r--app/helpers/dropdowns_helper.rb6
-rw-r--r--app/helpers/form_helper.rb4
-rw-r--r--app/helpers/groups_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb32
-rw-r--r--app/helpers/issues_helper.rb29
-rw-r--r--app/helpers/milestones_helper.rb2
-rw-r--r--app/helpers/milestones_routing_helper.rb8
-rw-r--r--app/helpers/namespaces_helper.rb39
-rw-r--r--app/helpers/nav_helper.rb10
-rw-r--r--app/helpers/notes_helper.rb8
-rw-r--r--app/helpers/projects_helper.rb10
-rw-r--r--app/helpers/system_note_helper.rb8
-rw-r--r--app/helpers/tab_helper.rb4
-rw-r--r--app/helpers/visibility_level_helper.rb77
-rw-r--r--app/mailers/base_mailer.rb4
-rw-r--r--app/models/application_setting.rb28
-rw-r--r--app/models/award_emoji.rb7
-rw-r--r--app/models/ci/build.rb7
-rw-r--r--app/models/ci/pipeline.rb5
-rw-r--r--app/models/ci/runner.rb12
-rw-r--r--app/models/ci/trigger_request.rb4
-rw-r--r--app/models/commit.rb24
-rw-r--r--app/models/commit_status.rb13
-rw-r--r--app/models/concerns/awardable.rb15
-rw-r--r--app/models/concerns/editable.rb2
-rw-r--r--app/models/concerns/issuable.rb11
-rw-r--r--app/models/concerns/milestoneish.rb8
-rw-r--r--app/models/concerns/noteable.rb6
-rw-r--r--app/models/concerns/spammable.rb2
-rw-r--r--app/models/dashboard_milestone.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/gpg_key.rb23
-rw-r--r--app/models/gpg_signature.rb14
-rw-r--r--app/models/group.rb45
-rw-r--r--app/models/group_milestone.rb2
-rw-r--r--app/models/issue.rb10
-rw-r--r--app/models/key.rb31
-rw-r--r--app/models/merge_request.rb33
-rw-r--r--app/models/milestone.rb6
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/network/graph.rb4
-rw-r--r--app/models/note.rb22
-rw-r--r--app/models/project.rb35
-rw-r--r--app/models/project_services/chat_notification_service.rb6
-rw-r--r--app/models/project_services/hipchat_service.rb6
-rw-r--r--app/models/protected_branch.rb2
-rw-r--r--app/models/repository.rb149
-rw-r--r--app/models/snippet.rb2
-rw-r--r--app/models/user.rb19
-rw-r--r--app/models/wiki_page.rb2
-rw-r--r--app/policies/base_policy.rb4
-rw-r--r--app/policies/project_policy.rb6
-rw-r--r--app/presenters/ci/build_presenter.rb11
-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/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/user_serializer.rb3
-rw-r--r--app/services/akismet_service.rb4
-rw-r--r--app/services/auth/container_registry_authentication_service.rb2
-rw-r--r--app/services/ci/create_pipeline_service.rb5
-rw-r--r--app/services/ci/create_trigger_request_service.rb19
-rw-r--r--app/services/ci/register_job_service.rb4
-rw-r--r--app/services/ci/retry_build_service.rb2
-rw-r--r--app/services/commits/create_service.rb2
-rw-r--r--app/services/compare_service.rb22
-rw-r--r--app/services/git_operation_service.rb159
-rw-r--r--app/services/git_push_service.rb10
-rw-r--r--app/services/issuable_base_service.rb16
-rw-r--r--app/services/issues/update_service.rb13
-rw-r--r--app/services/merge_requests/merge_service.rb6
-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/projects/after_import_service.rb24
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/projects/housekeeping_service.rb2
-rw-r--r--app/services/projects/update_pages_service.rb4
-rw-r--r--app/services/quick_actions/interpret_service.rb18
-rw-r--r--app/services/spam_service.rb2
-rw-r--r--app/services/system_note_service.rb2
-rw-r--r--app/services/upload_service.rb2
-rw-r--r--app/services/users/build_service.rb2
-rw-r--r--app/validators/key_restriction_validator.rb29
-rw-r--r--app/views/admin/application_settings/_form.html.haml68
-rw-r--r--app/views/admin/dashboard/_head.html.haml4
-rw-r--r--app/views/admin/monitoring/_head.html.haml4
-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/discussions/_headline.html.haml2
-rw-r--r--app/views/feature_highlight/_issue_boards.svg98
-rw-r--r--app/views/groups/issues.html.haml2
-rw-r--r--app/views/groups/milestones/show.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml10
-rw-r--r--app/views/layouts/header/_new.html.haml47
-rw-r--r--app/views/layouts/header/_new_dropdown.haml6
-rw-r--r--app/views/layouts/nav/_breadcrumbs.html.haml3
-rw-r--r--app/views/layouts/nav/_new_admin_sidebar.html.haml34
-rw-r--r--app/views/layouts/nav/_new_dashboard.html.haml49
-rw-r--r--app/views/layouts/nav/_new_explore.html.haml17
-rw-r--r--app/views/layouts/nav/_new_group_sidebar.html.haml10
-rw-r--r--app/views/layouts/nav/_new_profile_sidebar.html.haml24
-rw-r--r--app/views/layouts/nav/_new_project_sidebar.html.haml34
-rw-r--r--app/views/layouts/nav/_profile.html.haml2
-rw-r--r--app/views/layouts/nav/projects_dropdown/_show.html.haml15
-rw-r--r--app/views/layouts/project.html.haml8
-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/preferences/show.html.haml20
-rw-r--r--app/views/projects/_md_preview.html.haml5
-rw-r--r--app/views/projects/boards/_show.html.haml1
-rw-r--r--app/views/projects/boards/components/sidebar/_due_date.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_labels.html.haml2
-rw-r--r--app/views/projects/boards/components/sidebar/_milestone.html.haml2
-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/_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/issues/_discussion.html.haml14
-rw-r--r--app/views/projects/issues/_issues.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml13
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml8
-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/new.html.haml9
-rw-r--r--app/views/projects/notes/_actions.html.haml8
-rw-r--r--app/views/projects/runners/_form.html.haml6
-rw-r--r--app/views/projects/runners/show.html.haml3
-rw-r--r--app/views/projects/settings/_head.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/_visibility_level.html.haml2
-rw-r--r--app/views/shared/_visibility_radios.html.haml20
-rw-r--r--app/views/shared/icons/_caret_down.svg1
-rw-r--r--app/views/shared/icons/_icon_arrow_right.svg.erb1
-rw-r--r--app/views/shared/icons/_icon_resolve_discussion.svg1
-rw-r--r--app/views/shared/icons/_icon_status_success_solid.svg1
-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/_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/_search_bar.html.haml7
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml23
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.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.haml8
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml6
-rw-r--r--app/views/shared/milestones/_top.html.haml8
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml4
-rw-r--r--app/workers/create_gpg_signature_worker.rb6
-rw-r--r--app/workers/merge_worker.rb2
-rw-r--r--app/workers/stuck_ci_jobs_worker.rb2
-rw-r--r--changelogs/unreleased/17849-allow-admin-to-restrict-min-key-length-and-techno.yml5
-rw-r--r--changelogs/unreleased/26692-predefined-variable-gitlab-user-name.yml5
-rw-r--r--changelogs/unreleased/28202_decrease_abc_threshold_step3.yml5
-rw-r--r--changelogs/unreleased/28453-add-time-estimate-time-spent-to-api-issue-output.yml4
-rw-r--r--changelogs/unreleased/28938-password-change-workflow-for-admins.yml5
-rw-r--r--changelogs/unreleased/30162-retire-koding-integration.yml4
-rw-r--r--changelogs/unreleased/31273-creating-an-project-within-an-internal-sub-group-gives-the-option-to-set-it-a-public.yml6
-rw-r--r--changelogs/unreleased/31470-fix-api-files-raw.yml5
-rw-r--r--changelogs/unreleased/34261-move-move-to-sidebar.yml5
-rw-r--r--changelogs/unreleased/34413-move-convdev-index-location-to-after-cohorts.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/35048-empty-badges.yml5
-rw-r--r--changelogs/unreleased/35686-unescape-wiki-title.yml5
-rw-r--r--changelogs/unreleased/35793_fix_predicate_names.yml5
-rw-r--r--changelogs/unreleased/36061-mr-ref.yml5
-rw-r--r--changelogs/unreleased/36114-stuck-mrs-job-follow-up.yml4
-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/36860-deleted-user-fix.yml5
-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/37104-fix-graph-date-format.yml5
-rw-r--r--changelogs/unreleased/37147-fix-fallback-emoji-alignment.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/37331-button-MR-widget.yml5
-rw-r--r--changelogs/unreleased/37406-success-status-icon.yml5
-rw-r--r--changelogs/unreleased/add-filter-by-my-reaction.yml4
-rw-r--r--changelogs/unreleased/add_message_to_the_404_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/bugfix-notify-custom-participants.yml5
-rw-r--r--changelogs/unreleased/bvl-validate-po-files.yml4
-rw-r--r--changelogs/unreleased/check-trigger-permissions.yml5
-rw-r--r--changelogs/unreleased/docs-fix-15669-issue-move-api.yml5
-rw-r--r--changelogs/unreleased/dont-remove-add-diff-btn-on-post.yml5
-rw-r--r--changelogs/unreleased/feature-dependency-status-badge.yml5
-rw-r--r--changelogs/unreleased/feature-gb-kubernetes-only-pipeline-jobs.yml5
-rw-r--r--changelogs/unreleased/feature-gpg-verification-status.yml6
-rw-r--r--changelogs/unreleased/feature-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-gem-security-updates.yml5
-rw-r--r--changelogs/unreleased/fix-import-events.yml5
-rw-r--r--changelogs/unreleased/fix-npm-security-updates.yml5
-rw-r--r--changelogs/unreleased/fix_typo_in_deploy_keys_docs.yml5
-rw-r--r--changelogs/unreleased/fuzzy-issue-search.yml5
-rw-r--r--changelogs/unreleased/gitaly-post-upload-pack-mandatory.yml5
-rw-r--r--changelogs/unreleased/improve-autocomplete-user-performance.yml5
-rw-r--r--changelogs/unreleased/issue-api-my-reaction.yml5
-rw-r--r--changelogs/unreleased/issue-boards-breadcrumbs-container.yml5
-rw-r--r--changelogs/unreleased/mk-default-ldap-verify-certificates-secure.yml5
-rw-r--r--changelogs/unreleased/mk-fix-user-namespace-rename.yml5
-rw-r--r--changelogs/unreleased/move-action.yml4
-rw-r--r--changelogs/unreleased/mr-index-page-performance.yml5
-rw-r--r--changelogs/unreleased/perf-slow-issuable.yml6
-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/revert-appearances-description-html-not-null.yml5
-rw-r--r--changelogs/unreleased/rouge-2-2-1.yml5
-rw-r--r--changelogs/unreleased/sh-bump-jira-gem.yml5
-rw-r--r--changelogs/unreleased/sidebar-cache-updates.yml5
-rw-r--r--changelogs/unreleased/sm-cherry-pick-list-commits-in-message.yml5
-rw-r--r--changelogs/unreleased/zj-disable-pages-in-subgroups.yml5
-rw-r--r--changelogs/unreleased/zj-sort-templates.yml5
-rw-r--r--config/application.rb15
-rw-r--r--config/dependency_decisions.yml12
-rw-r--r--config/gitlab.yml.example7
-rw-r--r--config/initializers/1_settings.rb17
-rw-r--r--config/initializers/7_prometheus_metrics.rb3
-rw-r--r--config/initializers/8_metrics.rb1
-rw-r--r--config/initializers/fast_gettext.rb5
-rw-r--r--config/initializers/sentry.rb5
-rw-r--r--config/initializers/session_store.rb3
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/project.rb2
-rw-r--r--config/webpack.config.js4
-rw-r--r--db/migrate/20161020180657_add_minimum_key_length_to_application_settings.rb29
-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/20170817123339_add_verification_status_to_gpg_signatures.rb20
-rw-r--r--db/migrate/20170830125940_add_failure_reason_to_ci_builds.rb9
-rw-r--r--db/post_migrate/20170830084744_destroy_gpg_signatures.rb10
-rw-r--r--db/post_migrate/20170831195038_remove_valid_signature_from_gpg_signatures.rb11
-rw-r--r--db/schema.rb13
-rw-r--r--doc/README.md4
-rw-r--r--doc/administration/auth/ldap.md9
-rw-r--r--doc/administration/integration/koding.md6
-rw-r--r--doc/api/README.md12
-rw-r--r--doc/api/deploy_keys.md2
-rw-r--r--doc/api/issues.md153
-rw-r--r--doc/api/merge_requests.md123
-rw-r--r--doc/api/pipeline_schedules.md91
-rw-r--r--doc/api/runners.md9
-rw-r--r--doc/api/settings.md16
-rw-r--r--doc/api/users.md211
-rw-r--r--doc/articles/index.md1
-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/index.md2
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/docker/using_docker_build.md10
-rw-r--r--doc/ci/environments.md4
-rw-r--r--doc/ci/examples/README.md5
-rw-r--r--doc/ci/examples/code_climate.md6
-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.md2
-rw-r--r--doc/ci/yaml/README.md40
-rw-r--r--doc/development/fe_guide/style_guide_js.md121
-rw-r--r--doc/development/fe_guide/vue.md291
-rw-r--r--doc/development/i18n_guide.md41
-rw-r--r--doc/development/licensing.md5
-rw-r--r--doc/install/kubernetes/gitlab_chart.md14
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md73
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md24
-rw-r--r--doc/install/kubernetes/index.md60
-rw-r--r--doc/install/requirements.md4
-rw-r--r--doc/integration/README.md1
-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/user/permissions.md45
-rw-r--r--doc/user/project/gpg_signed_commits/img/project_signed_and_unsigned_commits.pngbin41193 -> 0 bytes
-rw-r--r--doc/user/project/gpg_signed_commits/img/project_signed_commit_unverified_signature.pngbin9542 -> 0 bytes
-rw-r--r--doc/user/project/gpg_signed_commits/img/project_signed_commit_verified_signature.pngbin14029 -> 0 bytes
-rw-r--r--doc/user/project/gpg_signed_commits/index.md246
-rw-r--r--doc/user/project/import/index.md1
-rw-r--r--doc/user/project/import/perforce.md50
-rw-r--r--doc/user/project/index.md2
-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/sidebar_move_issue.pngbin0 -> 54511 bytes
-rw-r--r--doc/user/project/issues/index.md4
-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/other_group_sees_shared_project.pngbin30182 -> 21154 bytes
-rw-r--r--doc/user/project/members/img/share_project_with_groups.pngbin30307 -> 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/share_project_with_groups.md19
-rw-r--r--doc/user/project/pipelines/job_artifacts.md17
-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/user/project/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.png (renamed from doc/user/project/gpg_signed_commits/img/profile_settings_gpg_keys_single_key.png)bin4403 -> 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.md246
-rw-r--r--doc/user/project/repository/index.md4
-rw-r--r--doc/user/search/img/issue_search_by_term.pngbin0 -> 127492 bytes
-rw-r--r--doc/user/search/index.md14
-rw-r--r--features/project/commits/user_lookup.feature16
-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/project/active_tab.rb16
-rw-r--r--features/steps/project/commits/user_lookup.rb49
-rw-r--r--features/steps/project/fork.rb2
-rw-r--r--features/steps/project/issues/issues.rb3
-rw-r--r--features/steps/project/issues/milestones.rb4
-rw-r--r--features/steps/project/merge_requests.rb2
-rw-r--r--features/steps/project/pages.rb7
-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/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.rb7
-rw-r--r--features/steps/shared/project_tab.rb4
-rw-r--r--features/support/gitaly.rb3
-rw-r--r--lib/after_commit_queue.rb2
-rw-r--r--lib/api/access_requests.rb10
-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.rb3
-rw-r--r--lib/api/commit_statuses.rb10
-rw-r--r--lib/api/deploy_keys.rb5
-rw-r--r--lib/api/deployments.rb2
-rw-r--r--lib/api/entities.rb56
-rw-r--r--lib/api/environments.rb5
-rw-r--r--lib/api/events.rb2
-rw-r--r--lib/api/files.rb14
-rw-r--r--lib/api/group_milestones.rb2
-rw-r--r--lib/api/group_variables.rb5
-rw-r--r--lib/api/groups.rb13
-rw-r--r--lib/api/helpers.rb21
-rw-r--r--lib/api/helpers/internal_helpers.rb4
-rw-r--r--lib/api/helpers/runner.rb2
-rw-r--r--lib/api/internal.rb17
-rw-r--r--lib/api/issues.rb37
-rw-r--r--lib/api/jobs.rb2
-rw-r--r--lib/api/labels.rb5
-rw-r--r--lib/api/members.rb10
-rw-r--r--lib/api/merge_request_diffs.rb2
-rw-r--r--lib/api/merge_requests.rb9
-rw-r--r--lib/api/notes.rb8
-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.rb16
-rw-r--r--lib/api/protected_branches.rb4
-rw-r--r--lib/api/repositories.rb2
-rw-r--r--lib/api/runner.rb12
-rw-r--r--lib/api/runners.rb13
-rw-r--r--lib/api/services.rb16
-rw-r--r--lib/api/settings.rb7
-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/todos.rb2
-rw-r--r--lib/api/triggers.rb5
-rw-r--r--lib/api/users.rb174
-rw-r--r--lib/api/v3/triggers.rb32
-rw-r--r--lib/api/variables.rb3
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb49
-rw-r--r--lib/email_template_interceptor.rb2
-rw-r--r--lib/github/import.rb82
-rw-r--r--lib/gitlab/asciidoc.rb2
-rw-r--r--lib/gitlab/auth.rb6
-rw-r--r--lib/gitlab/checks/force_push.rb2
-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.rb2
-rw-r--r--lib/gitlab/current_settings.rb2
-rw-r--r--lib/gitlab/database.rb8
-rw-r--r--lib/gitlab/database/grant.rb34
-rw-r--r--lib/gitlab/database/migration_helpers.rb36
-rw-r--r--lib/gitlab/git.rb1
-rw-r--r--lib/gitlab/git/operation_service.rb168
-rw-r--r--lib/gitlab/git/repository.rb203
-rw-r--r--lib/gitlab/git_access.rb9
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb2
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb14
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb16
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/gpg.rb2
-rw-r--r--lib/gitlab/gpg/commit.rb34
-rw-r--r--lib/gitlab/gpg/invalid_gpg_signature_updater.rb2
-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/issuables_count_for_state.rb50
-rw-r--r--lib/gitlab/key_fingerprint.rb48
-rw-r--r--lib/gitlab/metrics/influx_db.rb2
-rw-r--r--lib/gitlab/performance_bar.rb2
-rw-r--r--lib/gitlab/polling_interval.rb2
-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/reference_counter.rb44
-rw-r--r--lib/gitlab/sentry.rb4
-rw-r--r--lib/gitlab/shell.rb55
-rw-r--r--lib/gitlab/sql/pattern.rb46
-rw-r--r--lib/gitlab/ssh_public_key.rb71
-rw-r--r--lib/gitlab/template/base_template.rb6
-rw-r--r--lib/gitlab/usage_data.rb4
-rw-r--r--lib/gitlab/utils.rb4
-rw-r--r--lib/gitlab/workhorse.rb5
-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/base_check.rb2
-rw-r--r--lib/system_check/simple_executor.rb2
-rw-r--r--lib/tasks/gettext.rake42
-rw-r--r--lib/tasks/gitlab/check.rake1
-rw-r--r--lib/tasks/gitlab/cleanup.rake2
-rw-r--r--lib/tasks/gitlab/task_helpers.rb4
-rw-r--r--lib/tasks/import.rake2
-rw-r--r--locale/bg/gitlab.po384
-rw-r--r--locale/de/gitlab.po1014
-rw-r--r--locale/en/gitlab.po48
-rw-r--r--locale/eo/gitlab.po377
-rw-r--r--locale/es/gitlab.po187
-rw-r--r--locale/fr/gitlab.po429
-rw-r--r--locale/gitlab.pot35
-rw-r--r--locale/it/gitlab.po439
-rw-r--r--locale/ja/gitlab.po327
-rw-r--r--locale/ko/gitlab.po326
-rw-r--r--locale/pt_BR/gitlab.po406
-rw-r--r--locale/ru/gitlab.po393
-rw-r--r--locale/uk/gitlab.po386
-rw-r--r--locale/zh_CN/gitlab.po287
-rw-r--r--locale/zh_HK/gitlab.po286
-rw-r--r--locale/zh_TW/gitlab.po318
-rw-r--r--package.json2
-rw-r--r--public/404.html3
-rwxr-xr-xscripts/static-analysis5
-rw-r--r--spec/controllers/admin/users_controller_spec.rb32
-rw-r--r--spec/controllers/application_controller_spec.rb13
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb66
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb82
-rw-r--r--spec/controllers/passwords_controller_spec.rb8
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb228
-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/snippets_controller_spec.rb4
-rw-r--r--spec/controllers/projects_controller_spec.rb32
-rw-r--r--spec/controllers/snippets_controller_spec.rb4
-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/trigger_requests.rb9
-rw-r--r--spec/factories/gpg_signature.rb2
-rw-r--r--spec/factories/keys.rb49
-rw-r--r--spec/factories/projects.rb2
-rw-r--r--spec/features/admin/admin_active_tab_spec.rb8
-rw-r--r--spec/features/admin/admin_hooks_spec.rb4
-rw-r--r--spec/features/admin/admin_settings_spec.rb16
-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/commits_spec.rb101
-rw-r--r--spec/features/dashboard/active_tab_spec.rb25
-rw-r--r--spec/features/dashboard/issues_filter_spec.rb2
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb2
-rw-r--r--spec/features/groups/group_name_toggle_spec.rb51
-rw-r--r--spec/features/groups/group_settings_spec.rb4
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/groups_spec.rb2
-rw-r--r--spec/features/issues/award_emoji_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb12
-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.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb538
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb2
-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.rb37
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb109
-rw-r--r--spec/features/issues_spec.rb44
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb12
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb2
-rw-r--r--spec/features/merge_requests/diffs_spec.rb2
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb4
-rw-r--r--spec/features/merge_requests/user_posts_diff_notes_spec.rb12
-rw-r--r--spec/features/participants_autocomplete_spec.rb14
-rw-r--r--spec/features/profiles/account_spec.rb4
-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/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/guest_navigation_menu_spec.rb4
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb14
-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/user_requests_access_spec.rb5
-rw-r--r--spec/features/projects/new_project_spec.rb55
-rw-r--r--spec/features/projects/project_settings_spec.rb14
-rw-r--r--spec/features/projects/sub_group_issuables_spec.rb2
-rw-r--r--spec/features/projects/user_interacts_with_stars_spec.rb38
-rw-r--r--spec/features/projects_spec.rb43
-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.rb15
-rw-r--r--spec/features/search_spec.rb2
-rw-r--r--spec/features/signed_commits_spec.rb179
-rw-r--r--spec/features/task_lists_spec.rb15
-rw-r--r--spec/features/uploads/user_uploads_file_to_note_spec.rb1
-rw-r--r--spec/finders/issues_finder_spec.rb53
-rw-r--r--spec/finders/merge_requests_finder_spec.rb14
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request.json2
-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/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/blob_helper_spec.rb4
-rw-r--r--spec/helpers/issuables_helper_spec.rb106
-rw-r--r--spec/helpers/issues_helper_spec.rb10
-rw-r--r--spec/helpers/version_check_helper_spec.rb4
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb75
-rw-r--r--spec/javascripts/api_spec.js6
-rw-r--r--spec/javascripts/awards_handler_spec.js7
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js190
-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/blob.rb4
-rw-r--r--spec/javascripts/fixtures/branches.rb4
-rw-r--r--spec/javascripts/fixtures/dashboard.rb4
-rw-r--r--spec/javascripts/fixtures/deploy_keys.rb4
-rw-r--r--spec/javascripts/fixtures/issues.rb4
-rw-r--r--spec/javascripts/fixtures/jobs.rb4
-rw-r--r--spec/javascripts/fixtures/labels.rb4
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb9
-rw-r--r--spec/javascripts/fixtures/merge_requests_diffs.rb4
-rw-r--r--spec/javascripts/fixtures/projects.rb4
-rw-r--r--spec/javascripts/fixtures/prometheus_service.rb4
-rw-r--r--spec/javascripts/fixtures/raw.rb4
-rw-r--r--spec/javascripts/fixtures/services.rb4
-rw-r--r--spec/javascripts/fixtures/snippet.rb4
-rw-r--r--spec/javascripts/fixtures/todos.rb4
-rw-r--r--spec/javascripts/fly_out_nav_spec.js47
-rw-r--r--spec/javascripts/gl_dropdown_spec.js323
-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/merge_request_tabs_spec.js11
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js (renamed from spec/javascripts/monitoring/monitoring_spec.js)12
-rw-r--r--spec/javascripts/monitoring/dashboard_state_spec.js (renamed from spec/javascripts/monitoring/monitoring_state_spec.js)6
-rw-r--r--spec/javascripts/monitoring/graph/deployment_spec.js (renamed from spec/javascripts/monitoring/monitoring_deployment_spec.js)6
-rw-r--r--spec/javascripts/monitoring/graph/flag_spec.js (renamed from spec/javascripts/monitoring/monitoring_flag_spec.js)10
-rw-r--r--spec/javascripts/monitoring/graph/legend_spec.js107
-rw-r--r--spec/javascripts/monitoring/graph_row_spec.js (renamed from spec/javascripts/monitoring/monitoring_row_spec.js)21
-rw-r--r--spec/javascripts/monitoring/graph_spec.js (renamed from spec/javascripts/monitoring/monitoring_column_spec.js)50
-rw-r--r--spec/javascripts/monitoring/mock_data.js7580
-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/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/pipelines/navigation_tabs_spec.js127
-rw-r--r--spec/javascripts/pretty_time_spec.js81
-rw-r--r--spec/javascripts/project_select_combo_button_spec.js15
-rw-r--r--spec/javascripts/project_title_spec.js59
-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/shortcuts_issuable_spec.js124
-rw-r--r--spec/javascripts/shortcuts_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/vue_shared/components/identicon_spec.js (renamed from spec/javascripts/groups/group_identicon_spec.js)42
-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/ci/gitlab_ci_yaml_processor_spec.rb81
-rw-r--r--spec/lib/container_registry/tag_spec.rb2
-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/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/database/grant_spec.rb30
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb32
-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/git/repository_spec.rb72
-rw-r--r--spec/lib/gitlab/git_access_spec.rb38
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb232
-rw-r--r--spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb43
-rw-r--r--spec/lib/gitlab/gpg_spec.rb15
-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/safe_model_attributes.yml3
-rw-r--r--spec/lib/gitlab/issuables_count_for_state_spec.rb37
-rw-r--r--spec/lib/gitlab/key_fingerprint_spec.rb82
-rw-r--r--spec/lib/gitlab/reference_counter_spec.rb37
-rw-r--r--spec/lib/gitlab/sentry_spec.rb13
-rw-r--r--spec/lib/gitlab/shell_spec.rb64
-rw-r--r--spec/lib/gitlab/sql/pattern_spec.rb175
-rw-r--r--spec/lib/gitlab/ssh_public_key_spec.rb136
-rw-r--r--spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb18
-rw-r--r--spec/lib/gitlab/utils_spec.rb8
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb17
-rw-r--r--spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb79
-rw-r--r--spec/mailers/notify_spec.rb95
-rw-r--r--spec/models/application_setting_spec.rb59
-rw-r--r--spec/models/award_emoji_spec.rb36
-rw-r--r--spec/models/ci/build_spec.rb62
-rw-r--r--spec/models/ci/pipeline_spec.rb16
-rw-r--r--spec/models/ci/runner_spec.rb76
-rw-r--r--spec/models/ci/trigger_request_spec.rb17
-rw-r--r--spec/models/commit_spec.rb61
-rw-r--r--spec/models/commit_status_spec.rb21
-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.rb42
-rw-r--r--spec/models/container_repository_spec.rb2
-rw-r--r--spec/models/gpg_key_spec.rb38
-rw-r--r--spec/models/group_spec.rb77
-rw-r--r--spec/models/issue_spec.rb18
-rw-r--r--spec/models/key_spec.rb55
-rw-r--r--spec/models/merge_request_spec.rb53
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb13
-rw-r--r--spec/models/project_spec.rb37
-rw-r--r--spec/models/repository_spec.rb106
-rw-r--r--spec/models/user_spec.rb31
-rw-r--r--spec/models/wiki_page_spec.rb6
-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.rb7
-rw-r--r--spec/requests/api/commits_spec.rb4
-rw-r--r--spec/requests/api/deploy_keys_spec.rb4
-rw-r--r--spec/requests/api/environments_spec.rb4
-rw-r--r--spec/requests/api/files_spec.rb13
-rw-r--r--spec/requests/api/group_variables_spec.rb4
-rw-r--r--spec/requests/api/groups_spec.rb5
-rw-r--r--spec/requests/api/internal_spec.rb89
-rw-r--r--spec/requests/api/issues_spec.rb30
-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_requests_spec.rb16
-rw-r--r--spec/requests/api/notes_spec.rb12
-rw-r--r--spec/requests/api/pipeline_schedules_spec.rb167
-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.rb71
-rw-r--r--spec/requests/api/protected_branches_spec.rb4
-rw-r--r--spec/requests/api/runner_spec.rb61
-rw-r--r--spec/requests/api/runners_spec.rb16
-rw-r--r--spec/requests/api/settings_spec.rb14
-rw-r--r--spec/requests/api/snippets_spec.rb8
-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.rb20
-rw-r--r--spec/requests/api/users_spec.rb350
-rw-r--r--spec/requests/api/v3/commits_spec.rb6
-rw-r--r--spec/requests/api/v3/files_spec.rb4
-rw-r--r--spec/requests/api/v3/issues_spec.rb4
-rw-r--r--spec/requests/api/v3/project_snippets_spec.rb4
-rw-r--r--spec/requests/api/v3/snippets_spec.rb2
-rw-r--r--spec/requests/api/v3/triggers_spec.rb5
-rw-r--r--spec/serializers/note_entity_spec.rb51
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb40
-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.rb4
-rw-r--r--spec/services/git_push_service_spec.rb4
-rw-r--r--spec/services/issues/create_service_spec.rb4
-rw-r--r--spec/services/issues/update_service_spec.rb20
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb32
-rw-r--r--spec/services/projects/after_import_service_spec.rb55
-rw-r--r--spec/services/projects/create_service_spec.rb40
-rw-r--r--spec/services/projects/fork_service_spec.rb22
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb13
-rw-r--r--spec/services/projects/transfer_service_spec.rb20
-rw-r--r--spec/services/projects/update_pages_service_spec.rb1
-rw-r--r--spec/services/projects/update_service_spec.rb23
-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.rb30
-rw-r--r--spec/support/cycle_analytics_helpers.rb6
-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.rb9
-rw-r--r--spec/support/filtered_search_helpers.rb10
-rw-r--r--spec/support/javascript_fixtures_helpers.rb4
-rw-r--r--spec/support/notify_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/requests/api/status_shared_examples.rb25
-rw-r--r--spec/support/stub_env.rb2
-rw-r--r--spec/support/test_env.rb2
-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/help/index.html.haml_spec.rb1
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb4
-rw-r--r--spec/views/projects/commits/_commit.html.haml_spec.rb4
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb4
-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/tree/show.html.haml_spec.rb1
-rw-r--r--spec/views/shared/projects/_project.html.haml_spec.rb4
-rw-r--r--spec/workers/create_gpg_signature_worker_spec.rb9
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb6
-rw-r--r--spec/workers/merge_worker_spec.rb11
-rw-r--r--spec/workers/stuck_ci_jobs_worker_spec.rb22
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore2
-rw-r--r--vendor/gitignore/Haskell.gitignore1
-rw-r--r--vendor/gitignore/Prestashop.gitignore4
-rw-r--r--vendor/gitignore/Smalltalk.gitignore4
-rw-r--r--vendor/gitignore/Symfony.gitignore3
-rw-r--r--vendor/gitignore/VisualStudio.gitignore2
-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/Ruby.gitlab-ci.yml8
-rw-r--r--yarn.lock6
1019 files changed, 33121 insertions, 9677 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ab9627d4ab7..778d33fb960 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -125,6 +125,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
@@ -207,11 +208,10 @@ update-tests-metadata:
- '[[ -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}/all_node_*.json
+ - rm -f rspec_flaky/${CI_PROJECT_NAME}/*_node_*.json
flaky-examples-check:
<<: *dedicated-runner
- <<: *except-docs
image: ruby:2.3-alpine
services: []
before_script: []
@@ -226,6 +226,7 @@ flaky-examples-check:
- branches
except:
- master
+ - /(^docs[\/-].*|.*-docs$)/
artifacts:
expire_in: 30d
paths:
diff --git a/.rubocop.yml b/.rubocop.yml
index 23bb0fa8be8..16f2e4484fc 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -606,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:
@@ -631,7 +643,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method.
Metrics/CyclomaticComplexity:
Enabled: true
- Max: 16
+ Max: 15
# Limit lines to 80 characters.
Metrics/LineLength:
@@ -653,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 ########################################################################
@@ -1181,6 +1193,10 @@ GitlabSecurity/DeepMunge:
- '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:
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 4b4f14efea4..cdf97d1d842 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -237,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 ac0d22ced46..a02b6594fad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,24 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 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.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 12fb34b24be..6cc34f1de08 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -164,7 +164,7 @@ 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".
+~Geo, ~Gitaly, ~Platform, ~Prometheus, ~Release, and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index be386c9ede3..0f1a7dfc7c4 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.33.0
+0.37.0
diff --git a/Gemfile b/Gemfile
index dcdaf6cde7b..0341f2609ad 100644
--- a/Gemfile
+++ b/Gemfile
@@ -27,7 +27,7 @@ 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'
@@ -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'
@@ -184,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'
@@ -245,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'
@@ -284,7 +281,7 @@ group :metrics do
gem 'influxdb', '~> 0.2', require: false
# Prometheus
- gem 'prometheus-client-mmap', '~>0.7.0.beta12'
+ gem 'prometheus-client-mmap', '~>0.7.0.beta14'
gem 'raindrops', '~> 0.18'
end
@@ -337,7 +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.0.6', 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
@@ -352,6 +349,8 @@ group :development, :test do
gem 'activerecord_sane_schema_dumper', '0.2'
gem 'stackprof', '~> 0.2.10', require: false
+
+ gem 'simple_po_parser', '~> 1.1.2', require: false
end
group :test do
@@ -398,7 +397,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.31.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.32.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 0fd52337b23..320d42b8974 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -261,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)
@@ -275,7 +275,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.31.0)
+ gitaly-proto (0.32.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -283,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)
@@ -303,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)
@@ -329,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)
@@ -403,8 +404,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)
@@ -468,7 +470,7 @@ 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)
@@ -477,7 +479,7 @@ GEM
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
- mini_portile2 (2.1.0)
+ mini_portile2 (2.2.0)
minitest (5.7.0)
mmap2 (2.2.7)
mousetrap-rails (1.4.6)
@@ -491,8 +493,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)
@@ -515,9 +517,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)
@@ -605,7 +607,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
@@ -619,7 +621,7 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.7.0.beta12)
+ prometheus-client-mmap (0.7.0.beta14)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
coderay (~> 1.1.0)
@@ -722,7 +724,7 @@ GEM
retriable (1.4.1)
rinku (2.0.0)
rotp (2.1.2)
- rouge (2.2.0)
+ rouge (2.2.1)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -769,7 +771,7 @@ GEM
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
- rubocop-gitlab-security (0.0.6)
+ rubocop-gitlab-security (0.1.0)
rubocop (>= 0.47.1)
rubocop-rspec (1.15.1)
rubocop (>= 0.42.0)
@@ -832,6 +834,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)
@@ -871,7 +874,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)
@@ -890,9 +893,9 @@ 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)
@@ -1014,11 +1017,11 @@ 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-proto (~> 0.31.0)
+ gitaly-proto (~> 0.32.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
@@ -1040,7 +1043,7 @@ 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)
@@ -1060,7 +1063,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)
@@ -1068,7 +1071,7 @@ 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)
@@ -1093,7 +1096,7 @@ DEPENDENCIES
pg (~> 0.18.2)
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.7)
- prometheus-client-mmap (~> 0.7.0.beta12)
+ prometheus-client-mmap (~> 0.7.0.beta14)
pry-byebug (~> 3.4.1)
pry-rails (~> 0.3.4)
rack-attack (~> 4.4.1)
@@ -1125,7 +1128,7 @@ DEPENDENCIES
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
rubocop (~> 0.49.1)
- rubocop-gitlab-security (~> 0.0.6)
+ rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.15.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
@@ -1144,6 +1147,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)
@@ -1159,7 +1163,7 @@ 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)
unf (~> 0.1.4)
@@ -1174,4 +1178,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.15.3
+ 1.15.4
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/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 78cb3def879..8acddd6194c 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -5,7 +5,7 @@ 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',
+ projectsPath: '/api/:version/projects.json',
labelsPath: '/:namespace_path/:project_path/labels',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
@@ -58,6 +58,7 @@ const Api = {
const defaults = {
search: query,
per_page: 20,
+ simple: true,
};
if (gon.current_user_id) {
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/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/vue_shared/common_vue.js b/app/assets/javascripts/commons/vue.js
index eb2a6071fda..8b62d78c043 100644
--- a/app/assets/javascripts/vue_shared/common_vue.js
+++ b/app/assets/javascripts/commons/vue.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import './vue_resource_interceptor';
if (process.env.NODE_ENV !== 'production') {
Vue.config.productionTip = false;
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 5decfc1dc01..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 () {
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index b71c449090e..3dec4de06ec 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -74,6 +74,7 @@ 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';
@@ -98,7 +99,7 @@ import initChangesDropdown from './init_changes_dropdown';
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), {
@@ -171,7 +172,6 @@ import initChangesDropdown from './init_changes_dropdown';
shortcut_handler = new ShortcutsIssuable();
new ZenMode();
initIssuableSidebar();
- initNotes();
break;
case 'dashboard:milestones:index':
new ProjectSelect();
@@ -575,6 +575,7 @@ import initChangesDropdown from './init_changes_dropdown';
break;
case 'new':
new ProjectNew();
+ initProjectVisibilitySelector();
break;
case 'show':
new Star();
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/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 32cb42c8b10..063155a167a 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -1,4 +1,3 @@
-import Cookies from 'js-cookie';
import bp from './breakpoints';
const HIDE_INTERVAL_TIMEOUT = 300;
@@ -8,9 +7,12 @@ 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);
@@ -20,10 +22,8 @@ let headerHeight = 50;
export const getHeaderHeight = () => headerHeight;
export const canShowActiveSubItems = (el) => {
- const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md';
-
- if (el.classList.contains('active') && !isHiddenByMedia) {
- return Cookies.get('sidebar_collapsed') === 'true';
+ if (el.classList.contains('active') && (sidebar && !sidebar.classList.contains('sidebar-icons-only'))) {
+ return false;
}
return true;
@@ -142,14 +142,22 @@ export const documentMouseMove = (e) => {
if (mousePos.length > 6) mousePos.shift();
};
+export const subItemsMouseLeave = (relatedTarget) => {
+ clearTimeout(timeoutId);
+
+ if (!relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
+ hideMenu(currentOpenMenu);
+ }
+};
+
export default () => {
- const sidebar = document.querySelector('.sidebar-top-level-items');
+ sidebar = document.querySelector('.nav-sidebar');
if (!sidebar) return;
const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
- sidebar.addEventListener('mouseleave', () => {
+ sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
@@ -163,10 +171,7 @@ export default () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
if (subItems) {
- subItems.addEventListener('mouseleave', () => {
- clearTimeout(timeoutId);
- hideMenu(currentOpenMenu);
- });
+ subItems.addEventListener('mouseleave', e => subItemsMouseLeave(e.relatedTarget));
}
el.addEventListener('mouseenter', e => mouseEnterTopItems(e.currentTarget));
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index b62acfcd445..6f7671aa6fe 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -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 html = '<li class="dropdown-menu-empty-link"><a href="#" class="is-focused">No matching results</a></li>';
};
GitLabDropdown.prototype.rowClicked = function(el) {
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/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 3f848e0859b..470c39c6f76 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -10,8 +10,6 @@ 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..7c4f4da6127 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,12 +66,11 @@ 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]/, ''));
@@ -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/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index b8f4f4eaba3..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);
};
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 ff2b66046b4..283c0ec0410 100644
--- a/app/assets/javascripts/lib/utils/sticky.js
+++ b/app/assets/javascripts/lib/utils/sticky.js
@@ -1,5 +1,5 @@
export const isSticky = (el, scrollY, stickyTop) => {
- const top = el.offsetTop - scrollY;
+ const top = Math.floor(el.offsetTop - scrollY);
if (top <= stickyTop) {
el.classList.add('is-stuck');
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 6d7c7e3c930..f14458c8d41 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -102,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';
@@ -131,6 +132,7 @@ import './project_new';
import './project_select';
import './project_show';
import './project_variables';
+import './projects_dropdown';
import './projects_list';
import './syntax_highlight';
import './render_math';
@@ -248,7 +250,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/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 5a9b3d19f84..3b3620fe61b 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -253,6 +253,7 @@ import bp from './breakpoints';
loadDiff(source) {
if (this.diffsLoaded) {
+ document.dispatchEvent(new CustomEvent('scroll'));
return;
}
diff --git a/app/assets/javascripts/monitoring/components/monitoring.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index a6a2d3119e3..74244faa5d9 100644
--- a/app/assets/javascripts/monitoring/components/monitoring.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -3,8 +3,9 @@
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 GraphGroup from './graph_group.vue';
+ import GraphRow from './graph_row.vue';
+ import EmptyState from './empty_state.vue';
import MonitoringStore from '../stores/monitoring_store';
import eventHub from '../event_hub';
@@ -31,8 +32,9 @@
},
components: {
- monitoringRow,
- monitoringState,
+ GraphGroup,
+ GraphRow,
+ EmptyState,
},
methods: {
@@ -94,7 +96,6 @@
this.updatedAspectRatios = 0;
}
},
-
},
created() {
@@ -118,40 +119,27 @@
},
};
</script>
+
<template>
- <div
- class="prometheus-graphs"
- v-if="!showEmptyState">
- <div
- class="row"
+ <div v-if="!showEmptyState" class="prometheus-graphs">
+ <graph-group
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>
+ :key="index"
+ :name="groupData.group"
+ >
+ <graph-row
+ v-for="(row, index) in groupData.metrics"
+ :key="index"
+ :row-data="row"
+ :update-aspect-ratio="updateAspectRatio"
+ :deployment-data="store.deploymentData"
+ />
+ </graph-group>
</div>
- <monitoring-state
+ <empty-state
+ v-else
:selected-state="state"
:documentation-path="documentationPath"
:settings-path="settingsPath"
- v-else
/>
</template>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index 598021aa4df..a8708be76de 100644
--- a/app/assets/javascripts/monitoring/components/monitoring_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -62,49 +62,33 @@
},
};
</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 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">
+ <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 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}}
+ <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>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_column.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 407af51cb7a..9c785f4ada8 100644
--- a/app/assets/javascripts/monitoring/components/monitoring_column.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -1,19 +1,21 @@
<script>
import d3 from 'd3';
- import monitoringLegends from './monitoring_legends.vue';
- import monitoringFlag from './monitoring_flag.vue';
- import monitoringDeployment from './monitoring_deployment.vue';
+ 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 { formatRelevantDigits } from '../../lib/utils/number_utils';
+ 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: {
- columnData: {
+ graphData: {
type: Object,
required: true,
},
@@ -35,49 +37,47 @@
data() {
return {
+ baseGraphHeight: 450,
+ baseGraphWidth: 600,
graphHeight: 450,
graphWidth: 600,
graphHeightOffset: 120,
- xScale: {},
- yScale: {},
margin: {},
- data: [],
unitOfDisplay: '',
areaColorRgb: '#8fbce8',
lineColorRgb: '#1f78d1',
yAxisLabel: '',
legendTitle: '',
reducedDeploymentData: [],
- area: '',
- line: '',
measurements: measurements.large,
currentData: {
time: new Date(),
value: 0,
},
- currentYCoordinate: 0,
+ currentDataIndex: 0,
currentXCoordinate: 0,
currentFlagPosition: 0,
- metricUsage: '',
showFlag: false,
showDeployInfo: true,
+ timeSeries: [],
};
},
components: {
- monitoringLegends,
- monitoringFlag,
- monitoringDeployment,
+ GraphLegend,
+ GraphFlag,
+ GraphDeployment,
+ monitoringPaths,
},
computed: {
outterViewBox() {
- return `0 0 ${this.graphWidth} ${this.graphHeight}`;
+ return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
},
innerViewBox() {
- if ((this.graphWidth - 150) > 0) {
- return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`;
+ if ((this.baseGraphWidth - 150) > 0) {
+ return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
}
return '0 0 0 0';
},
@@ -88,7 +88,7 @@
paddingBottomRootSvg() {
return {
- paddingBottom: `${(Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0}%`,
+ paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
};
},
},
@@ -96,24 +96,23 @@
methods: {
draw() {
const breakpointSize = bp.getBreakpointSize();
- const query = this.columnData.queries[0];
+ 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.data = query.result[0].values;
this.unitOfDisplay = query.unit || '';
- this.yAxisLabel = this.columnData.y_label || 'Values';
+ 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;
- if (this.data !== undefined) {
- this.renderAxesPaths();
- this.formatDeployments();
- }
+ this.baseGraphHeight = this.graphHeight;
+ this.baseGraphWidth = this.graphWidth;
+ this.renderAxesPaths();
+ this.formatDeployments();
},
handleMouseOverGraph(e) {
@@ -122,16 +121,17 @@
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];
+ 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.currentXCoordinate = Math.floor(this.xScale(this.currentData.time));
+ this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
+ this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(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;
@@ -144,25 +144,34 @@
} else {
this.showFlag = true;
}
-
- this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`;
},
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]);
- this.yScale = d3.scale.linear()
+ const axisYScale = 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))]);
+
+ 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(this.yScale)
+ .scale(axisYScale)
.ticks(measurements.yTicks)
.orient('left');
@@ -178,25 +187,6 @@
.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);
},
},
@@ -222,7 +212,7 @@
:class="classType">
<h5
class="text-center graph-title">
- {{columnData.title}}
+ {{graphData.title}}
</h5>
<div
class="prometheus-svg-container"
@@ -238,57 +228,51 @@
class="y-axis"
transform="translate(70, 20)">
</g>
- <monitoring-legends
+ <graph-legend
:graph-width="graphWidth"
:graph-height="graphHeight"
:margin="margin"
:measurements="measurements"
- :area-color-rgb="areaColorRgb"
:legend-title="legendTitle"
:y-axis-label="yAxisLabel"
- :metric-usage="metricUsage"
+ :time-series="timeSeries"
+ :unit-of-display="unitOfDisplay"
+ :current-data-index="currentDataIndex"
/>
<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-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"
/>
- <monitoring-flag
+ <graph-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"
/>
+ <rect
+ class="prometheus-graph-overlay"
+ :width="(graphWidth - 70)"
+ :height="(graphHeight - 100)"
+ transform="translate(-5, 20)"
+ ref="graphOverlay"
+ @mousemove="handleMouseOverGraph($event)">
+ </rect>
</svg>
</svg>
</div>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
index e6432ba3191..3623d2ed946 100644
--- a/app/assets/javascripts/monitoring/components/monitoring_deployment.vue
+++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue
@@ -1,8 +1,5 @@
<script>
- import {
- dateFormat,
- timeFormat,
- } from '../constants';
+ import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
export default {
props: {
@@ -58,7 +55,7 @@
class="deploy-info"
v-if="showDeployInfo">
<g
- v-for="(deployment, index) in deploymentData"
+ v-for="(deployment, index) in deploymentData"
:key="index"
:class="nameDeploymentClass(deployment)"
:transform="transformDeploymentGroup(deployment)">
@@ -92,7 +89,7 @@
width="90"
height="58">
</rect>
- <g
+ <g
transform="translate(5, 2)">
<text
class="deploy-info-text text-metric-bold">
diff --git a/app/assets/javascripts/monitoring/components/monitoring_flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index 5a0e50fcab3..a98e3d06c18 100644
--- a/app/assets/javascripts/monitoring/components/monitoring_flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -1,8 +1,5 @@
<script>
- import {
- dateFormat,
- timeFormat,
- } from '../constants';
+ import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
export default {
props: {
@@ -10,10 +7,6 @@
type: Number,
required: true,
},
- currentYCoordinate: {
- type: Number,
- required: true,
- },
currentFlagPosition: {
type: Number,
required: true,
@@ -63,15 +56,6 @@
: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"
diff --git a/app/assets/javascripts/monitoring/components/monitoring_legends.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index 922a5e1bf0e..a43dad8e601 100644
--- a/app/assets/javascripts/monitoring/components/monitoring_legends.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -1,4 +1,6 @@
<script>
+ import { formatRelevantDigits } from '../../../lib/utils/number_utils';
+
export default {
props: {
graphWidth: {
@@ -17,10 +19,6 @@
type: Object,
required: true,
},
- areaColorRgb: {
- type: String,
- required: true,
- },
legendTitle: {
type: String,
required: true,
@@ -29,15 +27,25 @@
type: String,
required: true,
},
- metricUsage: {
+ 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: {
@@ -63,10 +71,28 @@
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;
});
@@ -74,7 +100,7 @@
};
</script>
<template>
- <g
+ <g
class="axis-label-container">
<line
class="label-x-axis-line"
@@ -100,7 +126,7 @@
:width="yLabelWidth"
:height="yLabelHeight">
</rect>
- <text
+ <text
class="label-axis-text y-label-text"
text-anchor="middle"
:transform="textTransform"
@@ -121,24 +147,33 @@
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 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..32c90fda8cc
--- /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">
+ <slot />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/monitoring_row.vue b/app/assets/javascripts/monitoring/components/graph_row.vue
index e5528f17880..bdb9149c3b4 100644
--- a/app/assets/javascripts/monitoring/components/monitoring_row.vue
+++ b/app/assets/javascripts/monitoring/components/graph_row.vue
@@ -1,5 +1,5 @@
<script>
- import monitoringColumn from './monitoring_column.vue';
+ import Graph from './graph.vue';
export default {
props: {
@@ -17,7 +17,7 @@
},
},
components: {
- monitoringColumn,
+ Graph,
},
computed: {
bootstrapClass() {
@@ -26,12 +26,12 @@
},
};
</script>
+
<template>
- <div
- class="prometheus-row row">
- <monitoring-column
- v-for="(column, index) in rowData"
- :column-data="column"
+ <div class="prometheus-row row">
+ <graph
+ v-for="(graphData, index) in rowData"
+ :graph-data="graphData"
:class-type="bootstrapClass"
:key="index"
:update-aspect-ratio="updateAspectRatio"
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/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..0a4cdd88044 100644
--- a/app/assets/javascripts/monitoring/stores/monitoring_store.js
+++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js
@@ -1,46 +1,52 @@
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,
+ })),
+ })),
+ })),
+ }));
+}
+
+function collate(array, rows = 2) {
+ const collatedArray = [];
+ let row = [];
+ array.forEach((value, index) => {
+ row.push(value);
+ if ((index + 1) % rows === 0) {
+ collatedArray.push(row);
+ row = [];
+ }
+ });
+ if (row.length > 0) {
+ collatedArray.push(row);
+ }
+ return collatedArray;
+}
+
+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: collate(normalizeMetrics(sortMetrics(group.metrics))),
+ }));
}
storeDeploymentData(deploymentData = []) {
@@ -57,5 +63,3 @@ class MonitoringStore {
return metricsCount;
}
}
-
-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 2d1ed9e4076..05e3f33f5ed 100644
--- a/app/assets/javascripts/new_sidebar.js
+++ b/app/assets/javascripts/new_sidebar.js
@@ -15,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() {
@@ -47,10 +48,13 @@ export default class NewNavSidebar {
if (this.$sidebar.length) {
this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
- this.$page.toggleClass('page-with-new-sidebar', !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() {
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/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
index 7695b04db74..3e5d6d15909 100644
--- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue
@@ -72,7 +72,7 @@
};
</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/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 5df317a76bf..010063a0240 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -139,7 +139,9 @@
};
</script>
<template>
- <div :class="cssClass">
+ <div
+ class="pipelines-container"
+ :class="cssClass">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index d7e3ab42f00..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) {
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 46a26fb91f4..99cea683d9a 100644
--- a/app/assets/javascripts/project_select_combo_button.js
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -14,7 +14,14 @@ export default class ProjectSelectComboButton {
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());
}
@@ -28,8 +35,9 @@ export default class ProjectSelectComboButton {
}
}
- 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() {
@@ -56,10 +64,8 @@ export default class ProjectSelectComboButton {
if (project) {
this.newItemBtn.attr('href', project.url);
this.newItemBtn.text(`${this.formattedText.defaultTextPrefix} in ${project.name}`);
- this.newItemBtn.enable();
} else {
this.newItemBtn.text(`Select project to create ${this.formattedText.presetTextSuffix}`);
- this.newItemBtn.disable();
}
}
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_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..fa5efef2919
--- /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|No projects matched your query');
+ },
+ },
+};
+</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..b71997234e5
--- /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 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/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index fa958d75fa4..4c87d46c96e 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -157,11 +157,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/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/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/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/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/groups/components/group_identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue
index 0edd820743f..7cf2e029cf6 100644
--- a/app/assets/javascripts/groups/components/group_identicon.vue
+++ b/app/assets/javascripts/vue_shared/components/identicon.vue
@@ -9,6 +9,11 @@ export default {
type: String,
required: true,
},
+ sizeClass: {
+ type: String,
+ required: false,
+ default: 's40',
+ },
},
computed: {
/**
@@ -38,7 +43,8 @@ export default {
<template>
<div
- class="avatar s40 identicon"
+ class="avatar identicon"
+ :class="sizeClass"
:style="identiconStyles">
{{identiconTitle}}
</div>
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/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index b2b3297e880..c0524bf6aa3 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -51,3 +51,4 @@
@import "framework/snippets";
@import "framework/memory_graph";
@import "framework/responsive-tables";
+@import "framework/feature_highlight";
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index b4a6b214e98..82350c36df0 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -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 4ce767e4cc4..c165ec0b94b 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -95,8 +95,8 @@
.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 e16fbbf43b5..a85051642dd 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; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index a45d5a6dca0..6b21def33a6 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -189,11 +189,11 @@
width: auto;
top: 100%;
left: 0;
- z-index: 9;
+ z-index: 200;
min-width: 240px;
max-width: 500px;
margin-top: 2px;
- margin-bottom: 0;
+ margin-bottom: 2px;
font-size: 14px;
font-weight: $gl-font-weight-normal;
padding: 8px 0;
@@ -368,6 +368,10 @@
transform: translateY(0);
}
+.comment-type-dropdown.open .dropdown-menu {
+ display: block;
+}
+
.filtered-search-box-input-container {
.dropdown-menu,
.dropdown-menu-nav {
@@ -618,6 +622,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;
@@ -728,7 +737,10 @@
@mixin new-style-dropdown($selector: '') {
#{$selector}.dropdown-menu,
#{$selector}.dropdown-menu-nav {
+ margin-bottom: 24px;
+
li {
+ display: block;
padding: 0 1px;
&:hover {
@@ -748,13 +760,18 @@
}
a,
- button {
+ 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 {
- @extend .text-danger;
+ color: $brand-danger;
}
&.is-focused,
@@ -763,6 +780,11 @@
&:focus {
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 {
@@ -771,6 +793,11 @@
&::before {
top: 16px;
}
+
+ &.dropdown-menu-user-link::before {
+ top: 50%;
+ transform: translateY(-50%);
+ }
}
}
}
@@ -791,4 +818,164 @@
#{$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%;
+ }
+ }
+ }
+}
+
+@include new-style-dropdown('.js-namespace-select + ');
+
+header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu {
+ padding: 0;
+
+ @media (max-width: $screen-xs-max) {
+ display: table;
+ left: -50px;
+ min-width: 300px;
+ }
+}
+
+.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 a5d33d410fb..b2847c348eb 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -225,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;
@@ -277,7 +289,7 @@
}
.filtered-search-input-dropdown-menu {
- max-height: 225px;
+ max-height: 260px;
max-width: 280px;
overflow: auto;
@@ -478,3 +490,7 @@
padding: 8px 16px;
text-align: center;
}
+
+.issues-details-filters {
+ @include new-style-dropdown;
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 35bd97980e2..b00a2d053e2 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -105,12 +105,11 @@ 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;
}
@@ -325,12 +324,12 @@ header {
li {
.badge {
position: inherit;
- top: -8px;
font-weight: $gl-font-weight-normal;
- margin-left: -11px;
+ 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/selects.scss b/app/assets/stylesheets/framework/selects.scss
index d93722e2174..6c14e8b97e0 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -266,11 +266,27 @@
}
// TODO: change global style
-.ajax-project-dropdown {
+.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,
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/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 71eec0e1a5e..3c0b4c82d19 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -10,8 +10,7 @@
color: $md-link-color;
}
- img {
- /*max-width: 100%;*/
+ img:not(.emoji) {
margin: 0 0 8px;
}
@@ -26,6 +25,7 @@
min-width: inherit;
min-height: inherit;
background-color: inherit;
+ max-width: 100%;
}
p a:not(.no-attachment-icon) img {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 8a2e64f7bf5..88b08998dfd 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -120,6 +120,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;
@@ -176,13 +177,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;
@@ -590,9 +592,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/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index 54fa4109f8b..4deb7431284 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -2,21 +2,35 @@
@import 'framework/tw_bootstrap_variables';
@import "bootstrap/variables";
+.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;
.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;
@@ -27,22 +41,16 @@ header.navbar-gitlab-new {
}
> 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;
+ margin-right: 8px;
}
}
@@ -51,7 +59,7 @@ header.navbar-gitlab-new {
svg {
width: 55px;
- height: 15px;
+ height: 14px;
margin: 0;
fill: $white-light;
}
@@ -59,9 +67,7 @@ header.navbar-gitlab-new {
&:hover,
&:focus {
- .logo-text svg {
- fill: $tanuki-yellow;
- }
+ background-color: rgba($indigo-200, .2);
}
}
}
@@ -81,6 +87,20 @@ 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 {
@@ -89,14 +109,10 @@ header.navbar-gitlab-new {
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,7 +126,7 @@ 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;
@@ -147,76 +163,167 @@ header.navbar-gitlab-new {
}
> a {
- background: none;
will-change: color;
+ margin: 4px 2px;
+ padding: 6px 8px;
+ color: $indigo-200;
+ height: 32px;
+
+ @media (max-width: $screen-xs-max) {
+ padding: 0;
+ }
+
+ svg {
+ fill: $indigo-200;
+ }
&.header-user-dropdown-toggle {
+ margin-left: 2px;
+
.header-user-avatar {
border-color: $indigo-200;
+ margin-right: 0;
}
}
+ }
- &:hover,
- &:focus {
- color: $white-light;
- opacity: 1;
+ .header-new-dropdown-toggle {
+ margin-right: 0;
+ }
- > svg {
- fill: $white-light;
- }
+ > a:hover,
+ > a:focus {
+ text-decoration: none;
+ outline: 0;
+ opacity: 1;
+ color: $white-light;
+
+ @media (min-width: $screen-sm-min) {
+ background-color: rgba($indigo-200, .2);
+ }
- &.header-user-dropdown-toggle {
- .header-user-avatar {
- border-color: $white-light;
- }
+ svg {
+ fill: currentColor;
+ }
+
+ &.header-user-dropdown-toggle {
+ .header-user-avatar {
+ border-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;
+
+ svg {
+ fill: $indigo-900;
+ }
+ }
+
+ .impersonation-btn,
+ .impersonation-btn:hover {
+ background-color: $white-light;
+ margin-left: 0;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+
+ i {
+ color: $orange-500;
+ font-size: 20px;
+ }
+ }
+
+ &.active > a,
+ &.dropdown.open > a {
+ color: $indigo-900;
+ background-color: $white-light;
+
+ svg {
+ fill: currentColor;
+ }
+ }
}
}
}
.navbar-sub-nav {
+ display: -webkit-flex;
display: flex;
- margin-bottom: 0;
+ margin: 0 0 0 6px;
color: $indigo-200;
- > li {
- > a:hover,
- > a:focus {
- box-shadow: inset 0 -3px 0 rgba($indigo-200, .4);
- text-decoration: none;
- outline: 0;
- color: $white-light;
- }
+ .dropdown-chevron {
+ position: relative;
+ top: -1px;
+ font-size: 10px;
+ }
+}
- &.active > a {
- box-shadow: inset 0 -3px 0 $indigo-500;
- color: $white-light;
- font-weight: $gl-font-weight-bold;
- }
+.navbar-gitlab-new {
+ .navbar-sub-nav,
+ .navbar-nav {
+ > li {
+ > a:hover,
+ > a:focus {
+ text-decoration: none;
+ outline: 0;
+ color: $white-light;
+ background-color: rgba($indigo-200, .2);
- > 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;
+ svg {
+ fill: currentColor;
+ }
+ }
- @media (min-width: $screen-sm-min) {
- padding: 15px $gl-padding;
- font-size: 14px;
+ &.active > a,
+ &.dropdown.open > a {
+ color: $indigo-900;
+ background-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 {
+ border-left: 1px solid rgba($indigo-200, .2);
+ margin: 8px;
}
}
}
+}
- .dropdown-chevron {
- position: relative;
- top: -1px;
- font-size: 10px;
- }
+.admin-icon i {
+ font-size: 18px;
+}
+
+.caret-down {
+ height: 11px;
+ width: 11px;
+ margin-left: 4px;
+ fill: currentColor;
}
.header-user .dropdown-menu-nav,
@@ -225,10 +332,14 @@ header.navbar-gitlab-new {
}
.search {
+ margin: 4px 8px 0;
+
form {
+ height: 32px;
border: 0;
+ border-radius: $border-radius-default;
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;
+ transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
background-color: rgba($indigo-200, .3);
@@ -237,31 +348,50 @@ header.navbar-gitlab-new {
}
&.search-active form {
- background-color: rgba($indigo-200, .3);
+ background-color: $white-light;
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;
+ height: 32px;
+ transition: border-color ease-in-out 0.15s;
}
.search-input-wrap {
@@ -273,8 +403,9 @@ header.navbar-gitlab-new {
&.search-active {
.location-badge {
- color: $white-light;
- background-color: rgba($indigo-200, .2);
+ color: $gl-text-color;
+ background-color: $nav-badge-bg;
+ border-color: $border-color;
}
.search-input-wrap {
@@ -448,3 +579,14 @@ header.navbar-gitlab-new {
}
}
}
+
+.btn-sign-in {
+ margin-top: 3px;
+ background-color: $indigo-100;
+ color: $indigo-900;
+ 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 f624b130e19..90b0a543c5c 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 {
@@ -93,7 +93,7 @@ $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;
background-color: $gray-normal;
@@ -189,7 +189,7 @@ $new-sidebar-collapsed-width: 50px;
}
.with-performance-bar .nav-sidebar {
- top: $header-height + $performance-bar-height;
+ top: $new-navbar-height + $performance-bar-height;
}
.sidebar-sub-level-items {
@@ -453,7 +453,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
@@ -464,7 +464,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});
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 3d04df8d820..50ec5110bf1 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -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 {
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 a8d2ae0af28..a52ac0d53e7 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -12,6 +12,8 @@
.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 {
@@ -249,8 +251,14 @@
font-weight: $gl-font-weight-bold;
}
- .label-axis-text,
- .text-metric-usage {
+ .label-axis-text {
+ fill: $black;
+ font-weight: $gl-font-weight-normal;
+ font-size: 10px;
+ }
+
+ .text-metric-usage,
+ .legend-metric-title {
fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 12px;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index ab5a901da71..9f2cb979518 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -473,7 +473,7 @@
padding-top: 6px;
}
- .open .dropdown-menu {
+ .dropdown-menu {
width: 100%;
}
}
@@ -486,6 +486,24 @@
}
}
+.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;
@@ -498,6 +516,7 @@
color: $gray-darkest;
display: block;
margin: 16px 0 0;
+ font-size: 85%;
.author_link {
color: $gray-darkest;
@@ -598,6 +617,8 @@
}
.issuable-actions {
+ @include new-style-dropdown;
+
padding-top: 10px;
@media (min-width: $screen-sm-min) {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index e2177f96aee..e8ca5cedaee 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -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 {
@@ -250,6 +247,10 @@ ul.related-merge-requests > li {
}
}
+.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/members.scss b/app/assets/stylesheets/pages/members.scss
index a385eb359e1..b3bab082a35 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -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) {
@@ -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 d1678a17aaf..8609f72bdab 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;
}
@@ -291,6 +280,7 @@
.dropdown-toggle {
.fa {
+ margin-left: 0;
color: inherit;
}
}
@@ -489,6 +479,8 @@
}
.mr-source-target {
+ @include new-style-dropdown;
+
display: flex;
flex-wrap: wrap;
justify-content: space-between;
@@ -610,6 +602,8 @@
}
.mr-version-controls {
+ @include new-style-dropdown;
+
position: relative;
background: $gray-light;
color: $gl-text-color;
@@ -727,3 +721,7 @@
font-size: 16px;
}
}
+
+.merge-request-form {
+ @include new-style-dropdown;
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 9558924bbcb..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;
}
@@ -202,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 0a194f3707f..45f2aed1531 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,6 +469,8 @@ ul.notes {
}
.note-actions {
+ @include new-style-dropdown;
+
align-self: flex-start;
flex-shrink: 0;
display: inline-flex;
@@ -488,22 +506,6 @@ ul.notes {
.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 {
@@ -766,17 +768,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;
}
}
@@ -806,10 +816,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/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index a408bde37d6..cb8815e4775 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;
}
@@ -206,8 +206,8 @@
.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 +219,7 @@
display: inline-block;
position: relative;
vertical-align: middle;
- height: 22px;
+ height: $ci-action-icon-size;
margin: 3px 0;
+ .stage-container {
@@ -257,6 +257,8 @@
// Pipeline visualization
.pipeline-actions {
+ @include new-style-dropdown;
+
border-bottom: none;
}
@@ -308,7 +310,7 @@
a {
text-decoration: none;
- color: $gl-text-color-secondary;
+ color: $gl-text-color;
}
svg {
@@ -432,7 +434,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 +451,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 +481,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 +499,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 +560,6 @@
// Triggers the dropdown in the big pipeline graph
.dropdown-counter-badge {
- color: $border-color;
font-weight: 100;
font-size: 15px;
position: absolute;
@@ -606,8 +601,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 +664,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: $gl-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 +785,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 +808,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;
}
@@ -927,3 +928,7 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
}
+
+.pipelines-container .top-area .nav-controls > .btn:last-child {
+ float: none;
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 39c4264e496..dd600a27545 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -299,28 +299,6 @@
}
}
-.project-visibility-level-holder {
- .radio {
- margin-bottom: 10px;
-
- i {
- margin: 2px 0;
- font-size: 20px;
- }
-
- .option-title {
- font-weight: $gl-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;
@@ -822,8 +800,10 @@ pre.light-well {
}
}
-.new_protected_branch,
+.new-protected-branch,
.new-protected-tag {
+ @include new-style-dropdown;
+
label {
margin-top: 6px;
font-weight: $gl-font-weight-normal;
@@ -843,19 +823,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: $gl-font-weight-bold;
- }
- }
+ margin-bottom: 30px;
.settings-message {
margin: 0;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 1088eca5322..efc47861768 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -99,6 +99,30 @@
.blob-viewer-container {
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 {
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 8d73246223d..615020ca856 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -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/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 5b9fafe31bd..6c8d87185e9 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -265,3 +265,7 @@
font-weight: $gl-font-weight-bold;
}
}
+
+.todos-filters {
+ @include new-style-dropdown;
+}
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index fa1bc72560e..a99563b7100 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -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
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 1d92ea11bda..97922e39ba8 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -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/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/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/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/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/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/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/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 8893a514207..dc9e6f71152 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
@@ -91,11 +90,25 @@ class Projects::IssuesController < Projects::ApplicationController
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)
@@ -257,6 +278,14 @@ class Projects::IssuesController < Projects::ApplicationController
return render_404 unless @project.feature_available?(:issues, current_user)
end
+ def render_issue_json
+ if @issue.valid?
+ render json: serializer.represent(@issue)
+ else
+ render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity
+ end
+ end
+
def issue_params
params.require(:issue).permit(*issue_params_attributes)
end
@@ -287,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_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 2a3b73577a5..5095d7fd445 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -18,10 +18,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] }
@@ -318,14 +317,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_controller.rb b/app/controllers/projects_controller.rb
index 1d24563a6a6..ed17b3b4689 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -20,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
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/helpers/application_helper.rb b/app/helpers/application_helper.rb
index bcee81bdc15..017df8f6794 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 = {})
@@ -303,7 +303,7 @@ module ApplicationHelper
end
def show_new_nav?
- cookies["new_nav"] == "true"
+ true
end
def collapsed_sidebar?
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 3b76da238e0..b93f5f0af1c 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]
@@ -113,6 +128,9 @@ 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,
@@ -156,6 +174,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/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/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/groups_helper.rb b/app/helpers/groups_helper.rb
index 4123a96911f..dd159d12aa0 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -68,7 +68,7 @@ module GroupsHelper
def group_title_link(group, hidable: false)
link_to(group_path(group), class: "group-path #{'hidable' if hidable}") do
output =
- if show_new_nav?
+ if show_new_nav? && !Rails.env.test?
image_tag(group_icon(group), class: "avatar-tile", width: 16, height: 16)
else
""
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 256de454ecc..49a69df7e5c 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)
@@ -207,12 +207,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,
@@ -229,7 +227,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,
@@ -240,16 +238,10 @@ module IssuablesHelper
}
end
- def issuables_count_for_state(issuable_type, state, finder: nil)
- finder ||= public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend
- cache_key = finder.state_counter_cache_key
+ def issuables_count_for_state(issuable_type, state)
+ finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend
- @counts ||= {}
- @counts[cache_key] ||= Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
- finder.count_by_state
- end
-
- @counts[cache_key][state]
+ Gitlab::IssuablesCountForState.new(finder)[state]
end
def close_issuable_url(issuable)
@@ -305,14 +297,6 @@ module IssuablesHelper
cookies[:collapsed_gutter] == 'true'
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
@@ -369,6 +353,8 @@ module IssuablesHelper
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..3d0fdce6a43 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?
@@ -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/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 86666022a2a..446a59030a6 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -164,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..73b3386fe9c 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -38,7 +38,7 @@ module NavHelper
end
def layout_nav_class
- return [] if show_new_nav?
+ return 'page-with-new-nav' if show_new_nav?
class_names = []
class_names << 'page-with-layout-nav' if defined?(nav) && nav
@@ -50,4 +50,12 @@ module NavHelper
def nav_control_class
"nav-control" if current_user
end
+
+ def user_dropdown_class
+ class_names = []
+ class_names << 'header-user-dropdown-toggle'
+ class_names << 'impersonated-user' if session[:impersonator_id]
+
+ class_names
+ end
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index e857e837c16..8c5e258f519 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -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
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index bee4950e414..02fe82ea872 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')
@@ -60,7 +62,7 @@ module ProjectsHelper
project_link = link_to project_path(project), { class: "project-item-select-holder" } do
output =
- if show_new_nav?
+ if show_new_nav? && !Rails.env.test?
project_icon(project, alt: project.name, class: 'avatar-tile', width: 16, height: 16)
else
""
@@ -70,12 +72,6 @@ module ProjectsHelper
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} / #{project_link}".html_safe
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/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/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/models/application_setting.rb b/app/models/application_setting.rb
index 8e446ff6dd8..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,6 +254,7 @@ 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,
@@ -413,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/ci/build.rb b/app/models/ci/build.rb
index 095192e9894..ba3156154ac 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
@@ -26,6 +27,7 @@ module Ci
validates :coverage, numericality: true, allow_blank: true
validates :ref, presence: true
+ validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
@@ -34,6 +36,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
@@ -387,7 +390,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
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 2d40f8012a3..35d14b6e297 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -36,6 +36,7 @@ module Ci
validates :sha, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
validates :status, presence: { unless: :importing? }
+ validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create
validate :valid_commit_sha, unless: :importing?
after_create :keep_around_commits, unless: :importing?
@@ -304,6 +305,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
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/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 d41c88b4e30..ba3845df867 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -251,6 +251,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}"
@@ -383,6 +405,6 @@ class Commit
end
def gpg_commit
- @gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self)
+ @gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 842c6e5cb50..f3888528940 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -38,6 +38,14 @@ class CommitStatus < ActiveRecord::Base
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
+ 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
@@ -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?
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/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/issuable.rb b/app/models/concerns/issuable.rb
index 3731b7c8577..681c3241dbb 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: [])
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/spammable.rb b/app/models/concerns/spammable.rb
index f2707022a4b..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
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/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/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 50fb35c77ec..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
@@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base
end
def gpg_commit
- Gitlab::Gpg::Commit.new(project, commit_sha)
+ Gitlab::Gpg::Commit.new(commit)
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index cb3ee032f69..190b27cf66b 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -26,6 +26,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 +104,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
- allowed_by_projects
+ def visibility_level_allowed_by_sub_groups?(level = self.visibility_level)
+ !children.where('visibility_level > ?', level).exists?
+ end
+
+ 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)
@@ -275,11 +286,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 b9aa937d2f9..8c7d492e605 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -269,7 +269,17 @@ 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
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/merge_request.rb b/app/models/merge_request.rb
index dbc73ed3cd4..724fb4ccef1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -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?
@@ -599,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
@@ -797,7 +805,7 @@ class MergeRequest < ActiveRecord::Base
end
def ref_path
- "refs/merge-requests/#{iid}/head"
+ "refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end
def ref_fetched?
@@ -819,7 +827,7 @@ class MergeRequest < ActiveRecord::Base
lock_mr
yield
ensure
- unlock_mr if locked?
+ unlock_mr
end
end
@@ -936,20 +944,19 @@ 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
private
def write_ref
- target_project.repository.with_repo_branch_commit(
- source_project.repository, source_branch) do |commit|
- if commit
- target_project.repository.write_ref(ref_path, commit.sha)
- else
- raise Rugged::ReferenceError, 'source repository is empty'
- end
- end
+ target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path)
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 e7bc1d1b080..e7cbc5170e8 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -195,6 +195,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.
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 0e5acb22d50..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 &&
diff --git a/app/models/note.rb b/app/models/note.rb
index a752c897d63..1073c115630 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -299,6 +299,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 +337,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/project.rb b/app/models/project.rb
index 8ade8c3fc38..01d04bc8d04 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -19,6 +19,7 @@ class Project < ActiveRecord::Base
include Routable
extend Gitlab::ConfigHelper
+ extend Gitlab::CurrentSettings
BoardLimitExceeded = Class.new(StandardError)
@@ -67,7 +68,6 @@ class Project < ActiveRecord::Base
acts_as_taggable
- attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
attr_accessor :template_name
attr_writer :pipeline_status
@@ -222,6 +222,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 }
@@ -372,11 +373,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
@@ -468,7 +465,7 @@ class Project < ActiveRecord::Base
end
def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage]['path']
+ Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
end
def team
@@ -583,7 +580,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)
@@ -1000,6 +997,20 @@ 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
@@ -1235,6 +1246,10 @@ class Project < ActiveRecord::Base
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 (
@@ -1494,6 +1509,10 @@ class Project < ActiveRecord::Base
self.storage_version.nil?
end
+ def renamed?
+ persisted? && path_changed?
+ end
+
private
def storage
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 7b15a5dd04d..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"
@@ -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 f422e0ea036..976d85246a8 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -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/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/repository.rb b/app/models/repository.rb
index 9fb2e2aa306..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.
@@ -60,6 +71,10 @@ class Repository
@project = project
end
+ def ==(other)
+ @disk_path == other.disk_path
+ end
+
def raw_repository
return nil unless full_path
@@ -75,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')
@@ -160,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
@@ -193,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
@@ -234,10 +232,10 @@ class Repository
begin
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
@@ -764,16 +762,30 @@ 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)
@@ -826,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
@@ -846,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
@@ -876,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
@@ -888,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,
@@ -901,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))
@@ -944,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
@@ -958,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
@@ -991,28 +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 = fetch_ref(
- start_repository.path_to_repo,
- "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}",
- "refs/tmp/#{SecureRandom.hex}/head"
- )
-
- 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
@@ -1027,17 +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_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
+ def fetch_source_branch(source_repository, source_branch, local_ref)
+ raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref)
+ end
- target_ref
+ 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)
@@ -1118,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
@@ -1159,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 = {})
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/user.rb b/app/models/user.rb
index fbd08bc4d0a..c5b5f09722f 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
@@ -303,7 +305,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
@@ -601,7 +603,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
@@ -642,11 +644,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)
@@ -664,10 +661,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
@@ -1048,6 +1041,10 @@ class User < ActiveRecord::Base
ensure_rss_token!
end
+ def verified_email?(email)
+ self.email == email
+ end
+
protected
# override, from Devise::Validatable
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/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/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/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/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 59153cbbc0a..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 = {
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/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index d0ba9f89460..414c01b2546 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -12,10 +12,11 @@ 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,
+ result = validate(current_user,
ignore_skip_ci: ignore_skip_ci,
save_on_errors: save_on_errors)
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 414f672cc6a..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?
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index ea3b8d66ed9..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|
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index dbd0b9ef43a..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, Gitlab::Git::HooksService::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/git_operation_service.rb b/app/services/git_operation_service.rb
deleted file mode 100644
index 6b7a56e6922..00000000000
--- a/app/services/git_operation_service.rb
+++ /dev/null
@@ -1,159 +0,0 @@
-class GitOperationService
- attr_reader :committer, :repository
-
- def initialize(committer, new_repository)
- committer = Gitlab::Git::Committer.from_user(committer) if committer.is_a?(User)
- @committer = committer
-
- @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)
- 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_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 e81a56672e2..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`.
@@ -108,7 +108,7 @@ class GitPushService < BaseService
# 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?
@@ -202,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/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 1486db046b5..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)
@@ -244,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')
@@ -340,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") # rubocop:disable GitlabSecurity/PublicSend
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
- end
end
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index a1f31abd164..b4ca3966505 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -6,7 +6,7 @@ module Issues
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)
@@ -74,6 +74,17 @@ 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, id)
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 5be749cd6a0..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
@@ -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/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/create_service.rb b/app/services/projects/create_service.rb
index a0cd52014a2..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
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index d66ef676088..dcef8b66215 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -26,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/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 394b336a638..d34903c9989 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
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index c7832c47e1a..9cdb9935bea 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -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_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/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/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/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 959af5c0d13..a010b4691bf 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,12 +42,7 @@
= link_to "(?)", help_page_path("integration/bitbucket")
and GitLab.com
= link_to "(?)", help_page_path("integration/gitlab")
- .form-group
- %label.control-label.col-sm-2 Enabled Git access protocols
- .col-sm-10
- = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
- %span.help-block#clone-protocol-help
- Allow only the selected protocols to be used for Git access.
+
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
@@ -55,6 +50,20 @@
= f.check_box :project_export_enabled
Project export enabled
+ .form-group
+ %label.control-label.col-sm-2 Enabled Git access protocols
+ .col-sm-10
+ = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
+ %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
@@ -153,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'
@@ -530,24 +539,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/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/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/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/discussions/_headline.html.haml b/app/views/discussions/_headline.html.haml
index c1dabeed387..25e90924413 100644
--- a/app/views/discussions/_headline.html.haml
+++ b/app/views/discussions/_headline.html.haml
@@ -5,7 +5,7 @@
by
= link_to_member(@project, discussion.resolved_by, avatar: false)
= 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/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/issues.html.haml b/app/views/groups/issues.html.haml
index 12bc092d216..837ef385dd5 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -12,6 +12,8 @@
- content_for :breadcrumbs_extra do
= link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10' do
= icon('rss')
+ %span.icon-label
+ Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues
- if group_issues_exists
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/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index b32cfe158bb..0d6760e7b8f 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -42,21 +42,21 @@
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
- %li
+ %li.user-counter
= 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
+ %li.user-counter
= 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
+ %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
@@ -74,8 +74,6 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li
= link_to "Settings", profile_path
- %li
- = link_to "Turn on new navigation", profile_preferences_path(anchor: "new-navigation")
%li.divider
%li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml
index 2c1c23d6ea9..61b71c091be 100644
--- a/app/views/layouts/header/_new.html.haml
+++ b/app/views/layouts/header/_new.html.haml
@@ -4,7 +4,7 @@
.header-content
.title-container
%h1.title
- = link_to root_path, title: 'Dashboard' do
+ = link_to root_path, title: 'Dashboard', id: 'logo' do
= brand_header_logo
%span.logo-text.hidden-xs
= render 'shared/logo_type.svg'
@@ -16,47 +16,35 @@
.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('chevron-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
@@ -68,15 +56,20 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li
= link_to "Settings", profile_path
- %li
- = link_to "Turn off new navigation", profile_preferences_path(anchor: "new-navigation")
+ - 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.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index 9da739b0974..9cf2739b368 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -1,11 +1,11 @@
%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')
+ = custom_icon('plus_square')
+ = custom_icon('caret_down')
- else
= icon('plus fw')
- = icon('caret-down')
+ = custom_icon('caret_down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
- if @group&.persisted?
diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml
index 4db84771f4e..653452871a0 100644
--- a/app/views/layouts/nav/_breadcrumbs.html.haml
+++ b/app/views/layouts/nav/_breadcrumbs.html.haml
@@ -1,8 +1,9 @@
- 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] }
+ .breadcrumbs-container{ class: [container, @content_class] }
- if defined?(@new_sidebar)
= button_tag class: 'toggle-mobile-nav', type: 'button' do
%span.sr-only Open sidebar
diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/_new_admin_sidebar.html.haml
index 1c3fd4a082c..3b53117deb6 100644
--- a/app/views/layouts/nav/_new_admin_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_admin_sidebar.html.haml
@@ -7,7 +7,7 @@
.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
- = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
+ = sidebar_link admin_root_path, title: _('Overview'), css: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('overview')
%span.nav-item-name
@@ -42,19 +42,19 @@
= 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
- = link_to admin_conversational_development_index_path, title: 'Monitoring' 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: :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
@@ -77,27 +77,28 @@
Requests Profiles
= nav_link(controller: :broadcast_messages) do
- = link_to admin_broadcast_messages_path, title: 'Messages' do
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do
.nav-icon-container
= custom_icon('abuse_reports')
%span.nav-item-name
@@ -106,43 +107,42 @@
- if akismet_enabled?
= nav_link(controller: :spam_logs) do
- = link_to admin_spam_logs_path, title: "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
= nav_link(controller: :deploy_keys) do
- = link_to admin_deploy_keys_path, title: '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
= nav_link(controller: :services) do
- = link_to admin_application_settings_services_path, title: 'Service Templates' 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
= nav_link(controller: :labels) do
- = link_to admin_labels_path, title: 'Labels' do
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link admin_application_settings_path, title: _('Settings') do
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
diff --git a/app/views/layouts/nav/_new_dashboard.html.haml b/app/views/layouts/nav/_new_dashboard.html.haml
index cfdfcbebc9f..8a39c4d775f 100644
--- a/app/views/layouts/nav/_new_dashboard.html.haml
+++ b/app/views/layouts/nav/_new_dashboard.html.haml
@@ -1,23 +1,38 @@
%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
+ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) 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']) do
+ = 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
Groups
- = nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm" }) do
+ = nav_link(path: 'dashboard#activity', html_options: { class: "visible-lg" }) do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
Activity
- %li.dropdown
+ = nav_link(controller: 'dashboard/milestones', html_options: { class: "visible-lg" }) do
+ = link_to dashboard_milestones_path, class: 'dashboard-shortcuts-milestones', title: 'Milestones' 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
+ Snippets
+
+ %li.dropdown.hidden-lg
%a{ href: "#", data: { toggle: "dropdown" } }
More
- = icon("chevron-down", class: "dropdown-chevron")
+ = custom_icon('caret_down')
.dropdown-menu
%ul
- = nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm" }) do
+ = 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
@@ -28,6 +43,20 @@
= 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'
+
+ -# 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/_new_explore.html.haml b/app/views/layouts/nav/_new_explore.html.haml
index 40385f251e3..cd1c39f3226 100644
--- a/app/views/layouts/nav/_new_explore.html.haml
+++ b/app/views/layouts/nav/_new_explore.html.haml
@@ -5,15 +5,8 @@
= 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'
+ = nav_link(controller: :snippets) do
+ = link_to explore_snippets_path, title: 'Snippets', class: 'dashboard-shortcuts-snippets' do
+ Snippets
+ %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
index d90aea2e361..5a1511b262f 100644
--- a/app/views/layouts/nav/_new_group_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_group_sidebar.html.haml
@@ -8,7 +8,7 @@
= @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
+ = sidebar_link group_path(@group), title: _('Group overview') do
.nav-icon-container
= custom_icon('project')
%span.nav-item-name
@@ -26,7 +26,7 @@
Activity
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
- = link_to issues_group_path(@group), title: 'Issues' do
+ = sidebar_link issues_group_path(@group), title: _('Issues') do
.nav-icon-container
= custom_icon('issues')
%span.nav-item-name
@@ -51,7 +51,7 @@
Milestones
= nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group), title: '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
@@ -59,14 +59,14 @@
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
+ = sidebar_link 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
+ = sidebar_link edit_group_path(@group), title: _('Settings') do
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml
index 85b2c7630c8..ccb6d1492f1 100644
--- a/app/views/layouts/nav/_new_profile_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml
@@ -7,76 +7,76 @@
.sidebar-context-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
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link 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
+ = sidebar_link audit_log_profile_path, title: _('Authentication log') do
.nav-icon-container
= custom_icon('authentication_log')
%span.nav-item-name
diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml
index 341943cf833..760c4c97c33 100644
--- a/app/views/layouts/nav/_new_project_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_project_sidebar.html.haml
@@ -9,7 +9,7 @@
= @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
+ = sidebar_link project_path(@project), title: _('Project overview'), css: 'shortcuts-project' do
.nav-icon-container
= custom_icon('project')
%span.nav-item-name
@@ -31,7 +31,7 @@
- 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
+ = sidebar_link project_tree_path(@project), title: _('Repository'), css: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('doc_text')
%span.nav-item-name
@@ -72,7 +72,7 @@
- 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
+ = 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
@@ -80,7 +80,7 @@
- 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
+ = sidebar_link project_issues_path(@project), title: _('Issues'), css: 'shortcuts-issues' do
.nav-icon-container
= custom_icon('issues')
%span.nav-item-name
@@ -99,6 +99,20 @@
= 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
@@ -112,7 +126,7 @@
- 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
+ = 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
@@ -122,7 +136,7 @@
- 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
+ = sidebar_link project_pipelines_path(@project), title: _('CI / CD'), css: 'shortcuts-pipelines' do
.nav-icon-container
= custom_icon('pipeline')
%span.nav-item-name
@@ -161,7 +175,7 @@
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
- = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
+ = sidebar_link get_project_wiki_path(@project), title: _('Wiki'), css: 'shortcuts-wiki' do
.nav-icon-container
= custom_icon('wiki')
%span.nav-item-name
@@ -169,7 +183,7 @@
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
- = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do
+ = sidebar_link project_snippets_path(@project), title: _('Snippets'), css: 'shortcuts-snippets' do
.nav-icon-container
= custom_icon('snippets')
%span.nav-item-name
@@ -177,7 +191,7 @@
- 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
+ = sidebar_link edit_project_path(@project), title: _('Settings'), css: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
@@ -208,7 +222,7 @@
= link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do
%span
CI / CD
- - 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/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 26d9640e98a..448f6abedf2 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -29,7 +29,7 @@
= link_to profile_emails_path, title: 'Emails' do
%span
Emails
- - if current_user.allow_password_authentication?
+ - unless current_user.ldap_user?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path, title: 'Password' do
%span
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/project.html.haml b/app/views/layouts/project.html.haml
index 54d56e9b873..d6db85ee87a 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -14,12 +14,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/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml
index 720a97cddb7..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('user/project/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/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index f08dcc0c242..9e7fe556d88 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -18,26 +18,6 @@
= scheme.name
.col-sm-12
%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
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 97041b87c48..71424593f2e 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,10 +1,5 @@
- referenced_users = local_assigns.fetch(:referenced_users, nil)
-- if defined?(@issue) && @issue.confidential?
- .confidential-issue-warning
- = confidential_icon(@issue)
- %span This is a confidential issue. Your comment will not be visible to the public.
-
.md-area
.md-header
%ul.nav-links.clearfix
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index 2076e46fde8..5354ec8522e 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -1,3 +1,4 @@
+- @no_breadcrumb_container = true
- @no_container = true
- @content_class = "issue-boards-content"
- page_title "Boards"
diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml
index f44a9d49a54..e8394eab213 100644
--- a/app/views/projects/boards/components/sidebar/_due_date.html.haml
+++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml
@@ -3,7 +3,7 @@
Due date
- if can?(current_user, :admin_issue, @project)
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "edit-link pull-right"
+ = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value
.value-content
%span.no-value{ "v-if" => "!issue.dueDate" }
diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml
index 7d0c35fe183..6b389736e8b 100644
--- a/app/views/projects/boards/components/sidebar/_labels.html.haml
+++ b/app/views/projects/boards/components/sidebar/_labels.html.haml
@@ -3,7 +3,7 @@
Labels
- if can?(current_user, :admin_issue, @project)
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "edit-link pull-right"
+ = 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
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
index 002e9994ee0..a1ddb261ea3 100644
--- a/app/views/projects/boards/components/sidebar/_milestone.html.haml
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -3,7 +3,7 @@
Milestone
- if can?(current_user, :admin_issue, @project)
= icon("spinner spin", class: "block-loading")
- = link_to "Edit", "#", class: "edit-link pull-right"
+ = link_to "Edit", "#", class: "js-sidebar-dropdown-toggle edit-link pull-right"
.value
%span.no-value{ "v-if" => "!issue.milestone" }
None
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/_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 a3783b31b86..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('user/project/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/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/show.html.haml b/app/views/projects/issues/show.html.haml
index de0f1de057d..fd7ff176c5e 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -2,6 +2,11 @@
- 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 +28,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
@@ -36,8 +41,8 @@
- if @issue.author && 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
@@ -74,7 +79,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 f5d5bc7eda9..43e23bb2200 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
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/new.html.haml b/app/views/projects/new.html.haml
index 647e0a772b1..adffd67029a 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -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 cb737d129f0..fb07141d2ac 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -26,8 +26,12 @@
":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?
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/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/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/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/_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/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/_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_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_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/_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/_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 f22b6c9a6c2..cb706d80f23 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
- elsif issuable.author
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 daa05990ae9..d8144a39b23 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/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index f63b9698408..e81789ea7a2 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -93,6 +93,13 @@
%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
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index c3f25c9d255..b07bc45512f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -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/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 6a85f7d0564..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,7 +31,7 @@
- if @group
.col-sm-6.milestone-actions
- if can?(current_user, :admin_milestones, @group)
- - if milestone.is_group_milestone?
+ - if milestone.group_milestone?
= link_to edit_group_milestone_path(@group, milestone), class: "btn btn-xs btn-grouped" do
Edit
\
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 40379f48393..f03e0ab154c 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -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 3014300fbe7..fd0760d83a5 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -22,7 +22,7 @@
- if group
.pull-right
- if can?(current_user, :admin_milestones, group)
- - if milestone.is_group_milestone?
+ - if milestone.group_milestone?
= link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do
Edit
- if milestone.active?
@@ -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/_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/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb
index f34dff2d656..9b5ff17aafa 100644
--- a/app/workers/create_gpg_signature_worker.rb
+++ b/app/workers/create_gpg_signature_worker.rb
@@ -6,7 +6,11 @@ class CreateGpgSignatureWorker
project = Project.find_by(id: project_id)
return unless project
+ commit = project.commit(commit_sha)
+
+ return unless commit
+
# This calculates and caches the signature in the database
- Gitlab::Gpg::Commit.new(project, commit_sha).signature
+ Gitlab::Gpg::Commit.new(commit).signature
end
end
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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/36061-mr-ref.yml b/changelogs/unreleased/36061-mr-ref.yml
deleted file mode 100644
index 039666070a7..00000000000
--- a/changelogs/unreleased/36061-mr-ref.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Instrument MergeRequest#ensure_ref_fetched
-merge_request:
-author:
-type: other
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/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/36860-deleted-user-fix.yml b/changelogs/unreleased/36860-deleted-user-fix.yml
deleted file mode 100644
index 79e75441134..00000000000
--- a/changelogs/unreleased/36860-deleted-user-fix.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix failure when issue is authored by a deleted user
-merge_request: 13807
-author:
-type: fixed
diff --git a/changelogs/unreleased/36882-disable-gitlab-project-import-button-if-source-disabled.yml b/changelogs/unreleased/36882-disable-gitlab-project-import-button-if-source-disabled.yml
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/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/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/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/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/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_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/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/bugfix-notify-custom-participants.yml b/changelogs/unreleased/bugfix-notify-custom-participants.yml
deleted file mode 100644
index 04fcb95e18a..00000000000
--- a/changelogs/unreleased/bugfix-notify-custom-participants.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fixed: Notifications weren't sending to participating users with a `Custom` notification setting.
-merge_request: 13680
-author: jneen
-type: fixed
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/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/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/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/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-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-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-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-gem-security-updates.yml b/changelogs/unreleased/fix-gem-security-updates.yml
new file mode 100644
index 00000000000..dce11d08402
--- /dev/null
+++ b/changelogs/unreleased/fix-gem-security-updates.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade mail and nokogiri gems due to security issues
+merge_request: 13662
+author: Markus Koller
+type: security
diff --git a/changelogs/unreleased/fix-import-events.yml b/changelogs/unreleased/fix-import-events.yml
deleted file mode 100644
index 84b4410a019..00000000000
--- a/changelogs/unreleased/fix-import-events.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix events error importing GitLab projects
-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_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/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-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/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/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/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-user-namespace-rename.yml b/changelogs/unreleased/mk-fix-user-namespace-rename.yml
deleted file mode 100644
index bb43b21f708..00000000000
--- a/changelogs/unreleased/mk-fix-user-namespace-rename.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make username update fail if the namespace update fails
-merge_request: 13642
-author:
-type: fixed
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-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/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/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/revert-appearances-description-html-not-null.yml b/changelogs/unreleased/revert-appearances-description-html-not-null.yml
deleted file mode 100644
index 4e3c39cb5fd..00000000000
--- a/changelogs/unreleased/revert-appearances-description-html-not-null.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Re-allow appearances.description_html to be NULL
-merge_request:
-author:
-type: fixed
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/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/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/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/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-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/config/application.rb b/config/application.rb
index f69dab4de39..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
)
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 25285525846..c5704ac5857 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -273,9 +273,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.
@@ -509,7 +508,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
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index abaabad5d65..360b72cdea3 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -155,18 +155,11 @@ 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
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 370a976b64a..5b455a8065a 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -122,6 +122,7 @@ def instrument_classes(instrumentation)
# 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/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/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/routes.rb b/config/routes.rb
index 5d7166cad9a..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'
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 3c7c218c356..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
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 7d63a42d7d8..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',
@@ -194,6 +195,7 @@ var config = {
'merge_conflicts',
'monitoring',
'notebook_viewer',
+ 'notes',
'pdf_viewer',
'pipelines',
'pipelines_details',
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/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/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/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/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/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 0f4b0c0c3b3..40b84f2bddd 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: 20170824162758) do
+ActiveRecord::Schema.define(version: 20170831195038) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -129,6 +129,10 @@ ActiveRecord::Schema.define(version: 20170824162758) do
t.boolean "password_authentication_enabled"
t.boolean "project_export_enabled", default: true, null: false
t.boolean "hashed_storage_enabled", default: false, null: false
+ 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
end
create_table "audit_events", force: :cascade do |t|
@@ -242,6 +246,8 @@ ActiveRecord::Schema.define(version: 20170824162758) do
t.integer "auto_canceled_by_id"
t.boolean "retried"
t.integer "stage_id"
+ 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
@@ -250,6 +256,7 @@ ActiveRecord::Schema.define(version: 20170824162758) do
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", ["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
@@ -332,6 +339,7 @@ ActiveRecord::Schema.define(version: 20170824162758) do
t.integer "auto_canceled_by_id"
t.integer "pipeline_schedule_id"
t.integer "source"
+ t.boolean "protected"
end
add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree
@@ -367,6 +375,7 @@ ActiveRecord::Schema.define(version: 20170824162758) 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
@@ -600,11 +609,11 @@ ActiveRecord::Schema.define(version: 20170824162758) do
t.datetime "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
diff --git a/doc/README.md b/doc/README.md
index 0dc5c4dc9ff..a59f71e83a5 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -77,6 +77,8 @@ 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)
@@ -98,7 +100,6 @@ 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](user/project/gpg_signed_commits/index.md): use GPG to sign your commits.
### Migrate and import your projects from other platforms
@@ -159,7 +160,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/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/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/api/README.md b/doc/api/README.md
index 266b5f018d9..a947eed2db8 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -61,16 +61,7 @@ following locations:
## 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
@@ -263,6 +254,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/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/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/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/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/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/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..57b4e117cf3 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -550,6 +550,217 @@ Parameters:
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
Get a list of currently authenticated user's emails.
diff --git a/doc/articles/index.md b/doc/articles/index.md
index 4b0c85b9272..798d4cbf4ff 100644
--- a/doc/articles/index.md
+++ b/doc/articles/index.md
@@ -26,6 +26,7 @@ Build, test, and deploy the software you develop with [GitLab CI/CD](../ci/READM
| Article title | Category | Publishing date |
| :------------ | :------: | --------------: |
+| [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 |
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/index.md b/doc/articles/numerous_undo_possibilities_in_git/index.md
index 9f1239b8f88..895bbccec08 100644
--- a/doc/articles/numerous_undo_possibilities_in_git/index.md
+++ b/doc/articles/numerous_undo_possibilities_in_git/index.md
@@ -3,7 +3,7 @@
> **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
+> **Publication date:** 2017-08-17
## Introduction
diff --git a/doc/ci/README.md b/doc/ci/README.md
index c722d895f42..1bf10e34ae7 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -112,6 +112,7 @@ Here is an collection of tutorials and guides on setting up your CI pipeline.
- [Run PHP Composer & NPM scripts then deploy them to a staging server](examples/deployment/composer-npm-deploy.md)
- [Analyze code quality with the Code Climate CLI](examples/code_climate.md)
- **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/)
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/environments.md b/doc/ci/environments.md
index 28b27921f8b..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.
---
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/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 234dc530db0..6513b31826a 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -78,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
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index abf4ec7dbf8..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
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 9c72fda0229..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
@@ -511,23 +510,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.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.2 You may use a jQuery dependency in Vue.js following [this example from the docs](https://vuejs.org/v2/examples/select2.html).
-1.3 If an outside jQuery Event needs to be listen to inside the Vue application, you may use jQuery event listeners.
-1.4 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. 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
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/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/licensing.md b/doc/development/licensing.md
index 2b16dfe0e7c..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,6 +64,7 @@ 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
@@ -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/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index 81057736e3a..a339bc23809 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -1,9 +1,9 @@
# GitLab Helm Chart
-> These Helm charts are in beta. GitLab is working on a [cloud-native](http://docs.gitlab.com/omnibus/package-information/cloud_native.html) set of [Charts](https://gitlab.com/charts/helm.gitlab.io) which will replace these.
-
-> Officially supported cloud providers are Google Container Service and Azure Container Service.
+> **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
@@ -428,7 +426,7 @@ 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.
-Ensure the GitLab repo has been added and re-initialize Helm:
+Add the GitLab Helm repository and initialize Helm:
```bash
helm repo add gitlab https://charts.gitlab.io
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index 05e0a59ffeb..d7fd8613633 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -1,7 +1,8 @@
# GitLab-Omnibus Helm Chart
-> These Helm charts are in beta. GitLab is working on a [cloud-native](http://docs.gitlab.com/omnibus/package-information/cloud_native.html) set of [Charts](https://gitlab.com/charts/helm.gitlab.io) which will replace these.
-
-> Officially supported cloud providers are Google Container Service and Azure Container Service.
+> **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.
@@ -20,62 +21,53 @@ The deployment includes:
A video demonstration of GitLab utilizing this chart [is available](https://about.gitlab.com/handbook/sales/demo/).
-Terms:
-
-- Google Cloud Platform (**GCP**)
-- Google Container Engine (**GKE**)
-- Azure Container Service (**ACS**)
-- Kubernetes (**k8s**)
-
## Prerequisites
-- _At least_ 4 GB of RAM available on your cluster, in chunks of 1 GB. 41GB of storage and 2 CPU are also required.
+- _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
-- An [external IP address](#networking-prerequisites)
- 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 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
### 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 external Load Balancer IP.
+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:
-- `baseIP`: the desired [external IP address](#networking-prerequisites)
-- `baseDomain`: the [base domain](#networking-prerequisites) with the wildcard host entry resolving to the `baseIP`. For example, `mycompany.io`.
+- `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 GCP, with `acs` also supported for Azure.
-- `legoEmail`: Email address to use when requesting new SSL certificates from Let's Encrypt
+- `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).
-These settings can either be passed directly 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
-```
-
-or within a YAML file:
-```bash
-helm install --name gitlab -f values.yaml gitlab/gitlab-omnibus
-```
-
-> **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](#persistent-storage).
-
### Choosing a different GitLab release version
The version of GitLab installed is based on the `gitlab` setting (see [section](#choosing-gitlab-edition) above), and
@@ -83,18 +75,16 @@ the value of the corresponding helm setting: `gitlabCEImage` or `gitabEEImage`.
```yaml
gitlab: CE
-gitlabCEImage: gitlab/gitlab-ce:9.1.2-ce.0
-gitlabEEImage: gitlab/gitlab-ee:9.1.2-ee.0
+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.
-> **Note:**
-There is no guarantee that other release versions of GitLab, other than what are
-used by default in the chart, will be supported by a chart install.
-
### 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).
@@ -124,9 +114,10 @@ Ingress routing and SSL are automatically configured within this Chart. An NGINX
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
-> 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.
+> **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.
-Ensure the GitLab repo has been added and re-initialize Helm:
+Add the GitLab Helm repository and initialize Helm:
```bash
helm repo add gitlab https://charts.gitlab.io
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index 51f94a33109..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
-> These Helm charts are in beta. GitLab is working on a [cloud-native](http://docs.gitlab.com/omnibus/package-information/cloud_native.html) set of [Charts](https://gitlab.com/charts/helm.gitlab.io) which will replace these.
-
-> Officially supported cloud providers are Google Container Service and Azure Container Service.
+> **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,7 +200,7 @@ certsSecretName: <SECRET NAME>
## Installing GitLab Runner using the Helm Chart
-Ensure the GitLab repo has been added and re-initialize Helm:
+Add the GitLab Helm repository and initialize Helm:
```bash
helm repo add gitlab https://charts.gitlab.io
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index eb98dc06a18..fb6c0c2d263 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -1,48 +1,58 @@
# Installing GitLab on Kubernetes
-> These Helm charts are in beta. GitLab is working on a [cloud-native](http://docs.gitlab.com/omnibus/package-information/cloud_native.html) set of [Charts](https://gitlab.com/charts/helm.gitlab.io) which will replace these.
-
> Officially supported cloud providers are Google Container Service and Azure Container Service.
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.
-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].
+It offers a quick way to configure and deploy the Runner on Kubernetes, regardless of where your GitLab server may be running.
-## Add the GitLab Helm repository
+### 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).
-Once Helm has been installed, the GitLab chart repository must be added:
+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.
-```bash
-helm repo add gitlab https://charts.gitlab.io
-```
+For most deployments we recommend using our [gitlab-omnibus](gitlab_omnibus.md) chart.
-After adding the repository, Helm must be re-initialized:
+## Upcoming Cloud Native Helm Charts
-```bash
-helm init
-```
+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).
-## Using the GitLab Helm Charts
+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.
-GitLab makes available three 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-omnibus](gitlab_omnibus.md): **Recommended** and the easiest way to get started. 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).
-- [gitlab](gitlab_chart.md): Just the GitLab service, with optional Postgres and Redis.
-- [gitlab-runner](gitlab_runner_chart.md): GitLab Runner, to process CI jobs.
+## Community Contributed Helm Charts
-We are also working on a new set of [cloud native Charts](https://gitlab.com/charts/helm.gitlab.io) which will eventually replace these.
+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 175dfc62096..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.
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/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/user/permissions.md b/doc/user/permissions.md
index dcf210e1085..bd0a58c4cca 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -21,16 +21,16 @@ 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,8 +71,8 @@ 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 | | | | | ✓ |
## Project features permissions
@@ -215,13 +215,13 @@ 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 | | | | |
@@ -243,12 +243,11 @@ with the permissions described on the documentation on [auditor users permission
Auditor users are available in [GitLab Enterprise Edition Premium](https://about.gitlab.com/gitlab-ee/)
only.
-----
-
-[^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.
+[^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/project/gpg_signed_commits/img/project_signed_and_unsigned_commits.png b/doc/user/project/gpg_signed_commits/img/project_signed_and_unsigned_commits.png
deleted file mode 100644
index 33936a7d6d7..00000000000
--- a/doc/user/project/gpg_signed_commits/img/project_signed_and_unsigned_commits.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/gpg_signed_commits/img/project_signed_commit_unverified_signature.png b/doc/user/project/gpg_signed_commits/img/project_signed_commit_unverified_signature.png
deleted file mode 100644
index 22565cf7c7e..00000000000
--- a/doc/user/project/gpg_signed_commits/img/project_signed_commit_unverified_signature.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/gpg_signed_commits/img/project_signed_commit_verified_signature.png b/doc/user/project/gpg_signed_commits/img/project_signed_commit_verified_signature.png
deleted file mode 100644
index 1778b2ddf2b..00000000000
--- a/doc/user/project/gpg_signed_commits/img/project_signed_commit_verified_signature.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/gpg_signed_commits/index.md b/doc/user/project/gpg_signed_commits/index.md
index 3ea2203c895..261eedeb412 100644
--- a/doc/user/project/gpg_signed_commits/index.md
+++ b/doc/user/project/gpg_signed_commits/index.md
@@ -1,245 +1 @@
-# 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 two prerequisites 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
-
-## Generating a GPG key
-
-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
+This document was moved to [another location](../repository/gpg_signed_commits/index.md).
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index 2a8728ed96e..67e856a97cd 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -7,6 +7,7 @@
1. [From Gitea](gitea.md)
1. [From SVN](svn.md)
1. [From ClearCase](clearcase.md)
+1. [From Perforce](perforce.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
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/index.md b/doc/user/project/index.md
index 41a96246292..d6b3d59d407 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -67,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
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/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/index.md b/doc/user/project/issues/index.md
index 20901e01f6e..0f187946a4a 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -86,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).
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/user/project/members/img/other_group_sees_shared_project.png b/doc/user/project/members/img/other_group_sees_shared_project.png
index 67af27043eb..e4c93a13abb 100644
--- a/doc/user/project/members/img/other_group_sees_shared_project.png
+++ b/doc/user/project/members/img/other_group_sees_shared_project.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
index 3cb4796f9f7..0907438cb84 100644
--- a/doc/user/project/members/img/share_project_with_groups.png
+++ 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/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md
index 4c1ddcdcba8..25e5b897825 100644
--- a/doc/user/project/members/share_project_with_groups.md
+++ b/doc/user/project/members/share_project_with_groups.md
@@ -5,7 +5,7 @@ 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
+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.
@@ -16,20 +16,23 @@ say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'P
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.
+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.
-![The 'Groups' section in the project settings screen](img/share_project_with_groups.png)
+![share project with groups](img/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.
+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
-!['Project Acme' is shared with 'Engineering' with a maximum access level of 'Developer'](img/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'.
+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 (EES/EEP)
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/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/user/project/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/user/project/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/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
index 5c14df36d73..5c14df36d73 100644
--- a/doc/user/project/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
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..afe8066d408
--- /dev/null
+++ b/doc/user/project/repository/gpg_signed_commits/index.md
@@ -0,0 +1,246 @@
+# 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
+
+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 5e5ae880518..235af83353d 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -22,7 +22,7 @@ you to [connect with GitLab via SSH](../../../ssh/README.md).
## Files
-## Create and edit 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
@@ -111,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/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 f5c7ce49e8e..21e96d8b11c 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -40,6 +40,20 @@ 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
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/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..8fb2ac34c32 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 .title' 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 .title' 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..20edcf75ff1 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('.breadcrumbs') do
+ click_link "New milestone"
+ end
end
step 'I press create mileston button' do
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..3b8d9af96c1 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -36,7 +36,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I goto the Merge Requests page' do
- page.within '.layout-nav' do
+ page.within '.nav-sidebar' do
click_link "Merge Requests"
end
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 2deef9036d3..f7dd4fc21e9 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -62,7 +62,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I click link "New issue"' do
- page.within '#content-body' do
+ page.within '.breadcrumbs' do
page.has_link?('New Issue') ? click_link('New Issue') : click_link('New issue')
end
end
@@ -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..307902a887e 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('.breadcrumbs') 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 7254fbc2e4e..3c3bffd7223 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 '.breadcrumbs' do
page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request')
end
end
diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb
index bb69c0d6e99..124a132d688 100644
--- a/features/steps/project/pages.rb
+++ b/features/steps/project/pages.rb
@@ -23,13 +23,13 @@ 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
@@ -37,7 +37,8 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps
step 'pages are deployed' do
pipeline = @project.pipelines.create(ref: 'HEAD',
sha: @project.commit('HEAD').sha,
- source: :push)
+ source: :push,
+ protected: false)
build = build(:ci_build,
project: @project,
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..100e674abed 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 .title' 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..96b7ba7549f 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 '.breadcrumbs' do
first(:link, "New snippet").click
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..2bb21a798aa 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')).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', 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 492da38355c..0cd7b506a95 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -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
@@ -150,15 +150,20 @@ module SharedNote
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/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/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
index b67575a3ac2..4750a2c373a 100644
--- a/lib/after_commit_queue.rb
+++ b/lib/after_commit_queue.rb
@@ -7,7 +7,7 @@ module AfterCommitQueue
end
def run_after_commit(method = nil, &block)
- _after_commit_queue << proc { self.send(method) } if method
+ _after_commit_queue << proc { self.send(method) } if method # rubocop:disable GitlabSecurity/PublicSend
_after_commit_queue << block if block
true
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/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..0b45621ce7b 100644
--- a/lib/api/broadcast_messages.rb
+++ b/lib/api/broadcast_messages.rb
@@ -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/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 e8dd61e493f..031dd02c6eb 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1,11 +1,11 @@
module API
module Entities
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
@@ -320,7 +320,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 +332,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 +387,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 +452,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 +491,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
@@ -737,6 +779,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? }
@@ -780,7 +823,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
@@ -801,6 +844,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 e2ac7142bc4..1598d3c00b8 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -1,5 +1,7 @@
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" }
@@ -58,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
@@ -75,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!
{
@@ -95,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)
@@ -113,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)
@@ -137,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 e56427304a6..31a918eda60 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -7,7 +7,11 @@ module API
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'
@@ -85,7 +89,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
@@ -118,8 +122,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 b56fd2388b3..3d377fdb9eb 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)
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index ecb79317093..f57ff0f2632 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -42,6 +42,10 @@ 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
+
private
def set_project
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..622bd9650e4 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
#
@@ -155,6 +155,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/jobs.rb b/lib/api/jobs.rb
index a40018b214e..5bab96398fd 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',
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..c3affcc6c6b 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
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/notes.rb b/lib/api/notes.rb
index 4e4e473994b..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,10 +129,12 @@ 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
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 15c3832b032..4845242a173 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
@@ -96,7 +95,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 +183,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
@@ -334,7 +333,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
@@ -363,8 +365,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
@@ -408,8 +409,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 dccf4fa27a7..15fcb9e8e27 100644
--- a/lib/api/protected_branches.rb
+++ b/lib/api/protected_branches.rb
@@ -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 31f940fe96b..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
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 667ba468ce6..851b226e9e5 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -122,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/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 e2019d6d512..1825c90a23b 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -230,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
@@ -287,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
@@ -299,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.'
@@ -403,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
@@ -481,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
@@ -533,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/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/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/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/github/import.rb b/lib/github/import.rb
index 7b848081e85..9354e142d3d 100644
--- a/lib/github/import.rb
+++ b/lib/github/import.rb
@@ -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/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 8cb4060cd97..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)
diff --git a/lib/gitlab/checks/force_push.rb b/lib/gitlab/checks/force_push.rb
index 714464fd5e7..dc5d285ea65 100644
--- a/lib/gitlab/checks/force_push.rb
+++ b/lib/gitlab/checks/force_push.rb
@@ -12,7 +12,7 @@ module Gitlab
!project
.repository
.gitaly_commit_client
- .is_ancestor(oldrev, newrev)
+ .ancestor?(oldrev, newrev)
else
Gitlab::Git::RevList.new(
path_to_repo: project.repository.path_to_repo,
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 d671867e7c7..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
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/database.rb b/lib/gitlab/database.rb
index e001d25e7b7..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
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 5e2c6cc5cad..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.
@@ -625,6 +629,30 @@ module Gitlab
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/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/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 b835dec24eb..75d4efc0bc5 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -17,6 +17,7 @@ module Gitlab
NoRepository = Class.new(StandardError)
InvalidBlobName = Class.new(StandardError)
InvalidRef = Class.new(StandardError)
+ GitError = Class.new(StandardError)
class << self
# Unlike `new`, `create` takes the storage path, not the storage name
@@ -46,6 +47,9 @@ module Gitlab
# Directory name of repo
attr_reader :name
+ # Relative path of repo
+ attr_reader :relative_path
+
# Rugged repo object
attr_reader :rugged
@@ -69,6 +73,10 @@ module Gitlab
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|
@@ -126,15 +134,19 @@ module Gitlab
# This is to work around a bug in libgit2 that causes in-memory refs to
# be stale/invalid when packed-refs is changed.
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/15392#note_14538333
- #
- # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/474
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
@@ -246,6 +258,19 @@ module Gitlab
branch_names + tag_names
end
+ 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
#
# - If no branches are present, returns nil
@@ -439,8 +464,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
@@ -584,6 +609,49 @@ 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
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/476
@@ -591,6 +659,23 @@ module Gitlab
rugged.branches.delete(branch_name)
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
#
# Examples:
@@ -706,6 +791,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
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 3e8b83c0f90..62d1ecae676 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -35,6 +35,7 @@ module Gitlab
def check(cmd, changes)
check_protocol!
+ check_valid_actor!
check_active_user!
check_project_accessibility!
check_project_moved!
@@ -70,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"
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 57f42bd35ee..21a32a7e0db 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -22,7 +22,7 @@ module Gitlab
end
end
- def is_ancestor(ancestor_id, child_id)
+ def ancestor?(ancestor_id, child_id)
request = Gitaly::CommitIsAncestorRequest.new(
repository: @gitaly_repo,
ancestor_id: ancestor_id,
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 8c0008c6971..a1a25cf2079 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -78,6 +78,20 @@ module Gitlab
raise ArgumentError, e.message
end
+ def find_branch(branch_name)
+ request = Gitaly::DeleteBranchRequest.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
+
private
def consume_refs_response(response)
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index a74a6dc6e78..177a1284f38 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -37,6 +37,22 @@ module Gitlab
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/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 45e9f9d65ae..025f826e65f 100644
--- a/lib/gitlab/gpg.rb
+++ b/lib/gitlab/gpg.rb
@@ -39,7 +39,7 @@ 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
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 606c7576f70..86bd9f5b125 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -1,17 +1,12 @@
module Gitlab
module Gpg
class Commit
- def self.for_commit(commit)
- new(commit.project, commit.sha)
- end
-
- def initialize(project, sha)
- @project = project
- @sha = sha
+ def initialize(commit)
+ @commit = commit
@signature_text, @signed_text =
begin
- Rugged::Commit.extract_signature(project.repository.rugged, sha)
+ Rugged::Commit.extract_signature(@commit.project.repository.rugged, @commit.sha)
rescue Rugged::OdbError
nil
end
@@ -26,7 +21,7 @@ module Gitlab
return @signature if @signature
- cached_signature = GpgSignature.find_by(commit_sha: @sha)
+ cached_signature = GpgSignature.find_by(commit_sha: @commit.sha)
return @signature = cached_signature if cached_signature.present?
@signature = create_cached_signature!
@@ -73,20 +68,31 @@ module Gitlab
def attributes(gpg_key)
user_infos = user_infos(gpg_key)
+ verification_status = verification_status(gpg_key)
{
- commit_sha: @sha,
- project: @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 a525ee7a9ee..e085eab26c9 100644
--- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
+++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb
@@ -8,7 +8,7 @@ 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 { |sig| sig.gpg_commit.update_signature!(sig) }
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/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/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/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/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/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/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/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 280a9abf03e..81ecdf43ef9 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -98,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
@@ -302,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)
@@ -325,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/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/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/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 9670c93759e..abb3d3a02c3 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -42,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 a362a3a0bc6..e5ad9b5a40c 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -35,10 +35,7 @@ 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
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/base_check.rb b/lib/system_check/base_check.rb
index 5dcb3f0886b..7f9e2ffffc2 100644
--- a/lib/system_check/base_check.rb
+++ b/lib/system_check/base_check.rb
@@ -73,7 +73,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/simple_executor.rb b/lib/system_check/simple_executor.rb
index e5986612908..6604b1078cf 100644
--- a/lib/system_check/simple_executor.rb
+++ b/lib/system_check/simple_executor.rb
@@ -53,7 +53,7 @@ module SystemCheck
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 1bd36bbe20a..92a3f503fcb 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
]
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/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
index d85b810ac66..8a63f486fa3 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -104,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
@@ -114,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/import.rake b/lib/tasks/import.rake
index 1206302cb76..4d485108cf6 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -80,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/locale/bg/gitlab.po b/locale/bg/gitlab.po
index 5c531f0cd7d..d8ab1f253e8 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 10:02-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,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 "Ðктивно"
@@ -56,19 +76,32 @@ 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 "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}"
@@ -77,14 +110,8 @@ 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 "ТърÑете в клоновете"
@@ -116,6 +143,9 @@ msgstr "ÐšÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ð½Ð° непрекъÑната интеграцÐ
msgid "Cancel"
msgstr "Отказ"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Избиране в клона"
@@ -194,6 +224,9 @@ msgstr "пропуÑнато"
msgid "CiStatus|running"
msgstr "протича в момента"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "Подаване"
@@ -241,12 +274,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 +310,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 "Програмиране"
@@ -332,9 +354,15 @@ msgstr[1] "ВнедрÑваниÑ"
msgid "Description"
msgstr "ОпиÑание"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "Име на папката"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "Да не Ñе показва повече"
@@ -371,6 +399,24 @@ msgstr "Редактиране"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Редактиране на плана %{id} за Ñхема"
+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 +462,13 @@ msgid "From issue creation until deploy to production"
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 "Към Вашето разклонение"
@@ -425,6 +476,24 @@ 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 "Ðачало"
@@ -434,12 +503,18 @@ msgstr "ОÑвежаването започна уÑпешно"
msgid "Import repository"
msgstr "ВнаÑÑне на хранилище"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Шаблон за интервала"
msgid "Introducing Cycle Analytics"
msgstr "ПредÑтавÑме Ви анализа на циклите"
+msgid "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "Задачи за поÑÐ»ÐµÐ´Ð½Ð¸Ñ Ð¼ÐµÑец"
@@ -469,6 +544,12 @@ msgstr "ПоÑледна промÑна"
msgid "Last commit"
msgstr "ПоÑледно подаване"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "Ðаучете повече в"
@@ -489,9 +570,15 @@ msgstr[1] "Ограничено до показване на най-много %
msgid "Median"
msgstr "Медиана"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "добавите SSH ключ"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Ðов проблем"
@@ -689,6 +776,9 @@ msgstr "Ñ ÐµÑ‚Ð°Ð¿"
msgid "Pipeline|with stages"
msgstr "Ñ ÐµÑ‚Ð°Ð¿Ð¸"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Проектът „%{project_name}“ е добавен в опашката за изтриване."
@@ -702,8 +792,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 +803,18 @@ 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 "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "Изключено"
@@ -746,6 +836,9 @@ msgstr "Етап"
msgid "ProjectNetworkGraph|Graph"
msgstr "Графика"
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Прочетете повече"
@@ -782,9 +875,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 "ОтмÑна на това подаване"
@@ -809,13 +914,14 @@ 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}."
-msgstr ""
-"Задайте парола на профила Ñи, за да можете да изтеглÑте и изпращате промени "
-"чрез %{protocol}."
+msgstr "Задайте парола на профила Ñи, за да можете да изтеглÑте и изпращате промени чрез %{protocol}."
msgid "Set up CI"
msgstr "ÐаÑтройка на ÐИ"
@@ -837,12 +943,18 @@ msgstr[1] "Показване на %d ÑъбитиÑ"
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 "Създайте %{new_merge_request} Ñ Ñ‚ÐµÐ·Ð¸ промени"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Преминаване към клон/етикет"
@@ -857,14 +969,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 +981,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 +1005,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 +1033,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 +1192,9 @@ msgstr "Качване на файл"
msgid "UploadLink|click to upload"
msgstr "щракнете за качване"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "Използване на глобалната Ви наÑтройка за извеÑтиÑта"
@@ -1167,22 +1226,13 @@ msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored!
msgstr "Ðа път Ñте да премахнете „%{group_name}“. Ðко Ñ Ð¿Ñ€ÐµÐ¼Ð°Ñ…Ð½ÐµÑ‚Ðµ, групата ÐЕ може да бъде възÑтановена! ÐÐИСТИÐРли иÑкате това?"
msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
-msgstr "Ðа път Ñте да премахнете „%{project_name_with_namespace}“. Ðко го премахнете, той ÐЕ може да бъде възÑтановен!"
-"ÐÐИСТИÐРли иÑкате това?"
+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 "Можете да добавÑте файлове Ñамо когато Ñе намирате в клон"
@@ -1202,31 +1252,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 "Вашето име"
@@ -1245,5 +1284,4 @@ msgstr "извеÑÑ‚Ð¸Ñ Ð¿Ð¾ е-поща"
msgid "parent"
msgid_plural "parents"
msgstr[0] "родител"
-msgstr[1] "родители"
-
+msgstr[1] "родители" \ No newline at end of file
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index ea864091b10..3cefb26d234 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -1,39 +1,321 @@
-# 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 09:29-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] ""
+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] ""
+msgstr[1] ""
+
+msgid "A collection of graphs regarding Continuous Integration"
+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 ""
+
+msgid "Activity"
+msgstr ""
+
+msgid "Add Changelog"
+msgstr ""
+
+msgid "Add Contribution guide"
+msgstr ""
+
+msgid "Add License"
+msgstr ""
+
+msgid "Add an SSH key to your profile to pull or push via SSH."
+msgstr ""
+
+msgid "Add new directory"
+msgstr ""
+
+msgid "All"
+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 "Branch"
+msgid_plural "Branches"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
+msgstr ""
+
+msgid "BranchSwitcherPlaceholder|Search branches"
+msgstr ""
+
+msgid "BranchSwitcherTitle|Switch branch"
+msgstr ""
+
+msgid "Branches"
+msgstr ""
+
+msgid "Browse Directory"
+msgstr ""
+
+msgid "Browse File"
+msgstr ""
+
+msgid "Browse Files"
+msgstr ""
+
+msgid "Browse files"
+msgstr ""
+
msgid "ByAuthor|by"
msgstr "Von"
+msgid "CI configuration"
+msgstr ""
+
msgid "Cancel"
msgstr ""
+msgid "Cancel edit"
+msgstr ""
+
+msgid "ChangeTypeActionLabel|Pick into branch"
+msgstr ""
+
+msgid "ChangeTypeActionLabel|Revert in branch"
+msgstr ""
+
+msgid "ChangeTypeAction|Cherry-pick"
+msgstr ""
+
+msgid "ChangeTypeAction|Revert"
+msgstr ""
+
+msgid "Changelog"
+msgstr ""
+
+msgid "Charts"
+msgstr ""
+
+msgid "Cherry-pick this commit"
+msgstr ""
+
+msgid "Cherry-pick this merge request"
+msgstr ""
+
+msgid "CiStatusLabel|canceled"
+msgstr ""
+
+msgid "CiStatusLabel|created"
+msgstr ""
+
+msgid "CiStatusLabel|failed"
+msgstr ""
+
+msgid "CiStatusLabel|manual action"
+msgstr ""
+
+msgid "CiStatusLabel|passed"
+msgstr ""
+
+msgid "CiStatusLabel|passed with warnings"
+msgstr ""
+
+msgid "CiStatusLabel|pending"
+msgstr ""
+
+msgid "CiStatusLabel|skipped"
+msgstr ""
+
+msgid "CiStatusLabel|waiting for manual action"
+msgstr ""
+
+msgid "CiStatusText|blocked"
+msgstr ""
+
+msgid "CiStatusText|canceled"
+msgstr ""
+
+msgid "CiStatusText|created"
+msgstr ""
+
+msgid "CiStatusText|failed"
+msgstr ""
+
+msgid "CiStatusText|manual"
+msgstr ""
+
+msgid "CiStatusText|passed"
+msgstr ""
+
+msgid "CiStatusText|pending"
+msgstr ""
+
+msgid "CiStatusText|skipped"
+msgstr ""
+
+msgid "CiStatus|running"
+msgstr ""
+
+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 ""
+
+msgid "Commit message"
+msgstr ""
+
+msgid "CommitBoxTitle|Commit"
+msgstr ""
+
+msgid "CommitMessage|Add %{file_name}"
+msgstr ""
+
+msgid "Commits"
+msgstr ""
+
+msgid "Commits feed"
+msgstr ""
+
+msgid "Commits|History"
+msgstr ""
+
+msgid "Committed by"
+msgstr ""
+
+msgid "Compare"
+msgstr ""
+
+msgid "Contribution guide"
+msgstr ""
+
+msgid "Contributors"
+msgstr ""
+
+msgid "Copy URL to clipboard"
+msgstr ""
+
+msgid "Copy commit SHA to clipboard"
+msgstr ""
+
+msgid "Create New Directory"
+msgstr ""
+
+msgid "Create a new branch"
+msgstr ""
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr ""
+
+msgid "Create directory"
+msgstr ""
+
+msgid "Create empty bare repository"
+msgstr ""
+
+msgid "Create merge request"
+msgstr ""
+
+msgid "Create new..."
+msgstr ""
+
+msgid "CreateNewFork|Fork"
+msgstr ""
+
+msgid "CreateTag|Tag"
+msgstr ""
+
+msgid "CreateTokenToCloneLink|create a personal access token"
+msgstr ""
msgid "Cron Timezone"
msgstr ""
+msgid "Cron syntax"
+msgstr ""
+
+msgid "Custom notification events"
+msgstr ""
+
+msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
+msgstr ""
+
+msgid "Cycle Analytics"
+msgstr ""
+
msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr "Cycle Analytics liefern einen Überblick darüber, wie viel Zeit in Ihrem Projekt von einer Idee bis zum Produktivdeployment vergeht."
@@ -58,6 +340,9 @@ msgstr "Staging"
msgid "CycleAnalyticsStage|Test"
msgstr "Test"
+msgid "Define a custom pattern with cron syntax"
+msgstr ""
+
msgid "Delete"
msgstr ""
@@ -69,19 +354,94 @@ msgstr[1] "Deployments"
msgid "Description"
msgstr ""
+msgid "Details"
+msgstr ""
+
+msgid "Directory name"
+msgstr ""
+
+msgid "Discard changes"
+msgstr ""
+
+msgid "Don't show again"
+msgstr ""
+
+msgid "Download"
+msgstr ""
+
+msgid "Download tar"
+msgstr ""
+
+msgid "Download tar.bz2"
+msgstr ""
+
+msgid "Download tar.gz"
+msgstr ""
+
+msgid "Download zip"
+msgstr ""
+
+msgid "DownloadArtifacts|Download"
+msgstr ""
+
+msgid "DownloadCommit|Email Patches"
+msgstr ""
+
+msgid "DownloadCommit|Plain Diff"
+msgstr ""
+
+msgid "DownloadSource|Download"
+msgstr ""
+
msgid "Edit"
msgstr ""
msgid "Edit Pipeline Schedule %{id}"
msgstr ""
+msgid "EventFilterBy|Filter by all"
+msgstr ""
+
+msgid "EventFilterBy|Filter by comments"
+msgstr ""
+
+msgid "EventFilterBy|Filter by issue events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by merge events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by push events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by team"
+msgstr ""
+
+msgid "Every day (at 4:00am)"
+msgstr ""
+
+msgid "Every month (on the 1st at 4:00am)"
+msgstr ""
+
+msgid "Every week (Sundays at 4:00am)"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr ""
msgid "Failed to remove the pipeline schedule"
msgstr ""
-msgid "Filter"
+msgid "Files"
+msgstr ""
+
+msgid "Filter by commit message"
+msgstr ""
+
+msgid "Find by path"
+msgstr ""
+
+msgid "Find file"
msgstr ""
msgid "FirstPushedBy|First"
@@ -90,18 +450,86 @@ msgstr "Erster"
msgid "FirstPushedBy|pushed by"
msgstr "gepusht von"
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "ForkedFromProjectPath|Forked from"
+msgstr ""
+
msgid "From issue creation until deploy to production"
msgstr "Vom Anlegen des Issues bis zum Produktivdeployment"
msgid "From merge request merge until deploy to production"
msgstr "Vom Merge Request bis zum Produktivdeployment"
+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 ""
+
+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 "Was sind Cycle Analytics?"
+msgid "Issue events"
+msgstr ""
+
+msgid "Jobs for last month"
+msgstr ""
+
+msgid "Jobs for last week"
+msgstr ""
+
+msgid "Jobs for last year"
+msgstr ""
+
+msgid "LFSStatus|Disabled"
+msgstr ""
+
+msgid "LFSStatus|Enabled"
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] "Letzter %d Tag"
@@ -110,13 +538,46 @@ msgstr[1] "Letzten %d Tage"
msgid "Last Pipeline"
msgstr ""
+msgid "Last Update"
+msgstr ""
+
+msgid "Last commit"
+msgstr ""
+
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
+msgid "Learn more in the"
+msgstr ""
+
+msgid "Learn more in the|pipeline schedules documentation"
+msgstr ""
+
+msgid "Leave group"
+msgstr ""
+
+msgid "Leave project"
+msgstr ""
+
msgid "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"
msgid "Median"
-msgstr "Median"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "MissingSSHKeyWarningLink|add an SSH key"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
msgid "New Issue"
msgid_plural "New Issues"
@@ -126,6 +587,33 @@ msgstr[1] "Neue Issues"
msgid "New Pipeline Schedule"
msgstr ""
+msgid "New branch"
+msgstr ""
+
+msgid "New directory"
+msgstr ""
+
+msgid "New file"
+msgstr ""
+
+msgid "New issue"
+msgstr ""
+
+msgid "New merge request"
+msgstr ""
+
+msgid "New schedule"
+msgstr ""
+
+msgid "New snippet"
+msgstr ""
+
+msgid "New tag"
+msgstr ""
+
+msgid "No repository"
+msgstr ""
+
msgid "No schedules"
msgstr ""
@@ -135,12 +623,75 @@ msgstr "Nicht verfügbar"
msgid "Not enough data"
msgstr "Nicht genügend Daten"
+msgid "Notification events"
+msgstr ""
+
+msgid "NotificationEvent|Close issue"
+msgstr ""
+
+msgid "NotificationEvent|Close merge request"
+msgstr ""
+
+msgid "NotificationEvent|Failed pipeline"
+msgstr ""
+
+msgid "NotificationEvent|Merge merge request"
+msgstr ""
+
+msgid "NotificationEvent|New issue"
+msgstr ""
+
+msgid "NotificationEvent|New merge request"
+msgstr ""
+
+msgid "NotificationEvent|New note"
+msgstr ""
+
+msgid "NotificationEvent|Reassign issue"
+msgstr ""
+
+msgid "NotificationEvent|Reassign merge request"
+msgstr ""
+
+msgid "NotificationEvent|Reopen issue"
+msgstr ""
+
+msgid "NotificationEvent|Successful pipeline"
+msgstr ""
+
+msgid "NotificationLevel|Custom"
+msgstr ""
+
+msgid "NotificationLevel|Disabled"
+msgstr ""
+
+msgid "NotificationLevel|Global"
+msgstr ""
+
+msgid "NotificationLevel|On mention"
+msgstr ""
+
+msgid "NotificationLevel|Participate"
+msgstr ""
+
+msgid "NotificationLevel|Watch"
+msgstr ""
+
+msgid "OfSearchInADropdown|Filter"
+msgstr ""
+
msgid "OpenedNDaysAgo|Opened"
msgstr "Erstellt"
+msgid "Options"
+msgstr ""
+
msgid "Owner"
msgstr ""
+msgid "Pipeline"
+msgstr ""
+
msgid "Pipeline Health"
msgstr "Pipeline Kennzahlen"
@@ -150,6 +701,21 @@ msgstr ""
msgid "Pipeline Schedules"
msgstr ""
+msgid "PipelineCharts|Failed:"
+msgstr ""
+
+msgid "PipelineCharts|Overall statistics"
+msgstr ""
+
+msgid "PipelineCharts|Success ratio:"
+msgstr ""
+
+msgid "PipelineCharts|Successful:"
+msgstr ""
+
+msgid "PipelineCharts|Total:"
+msgstr ""
+
msgid "PipelineSchedules|Activated"
msgstr ""
@@ -162,6 +728,12 @@ msgstr ""
msgid "PipelineSchedules|Inactive"
msgstr ""
+msgid "PipelineSchedules|Input variable key"
+msgstr ""
+
+msgid "PipelineSchedules|Input variable value"
+msgstr ""
+
msgid "PipelineSchedules|Next Run"
msgstr ""
@@ -171,18 +743,114 @@ msgstr ""
msgid "PipelineSchedules|Provide a short description for this pipeline"
msgstr ""
+msgid "PipelineSchedules|Remove variable row"
+msgstr ""
+
msgid "PipelineSchedules|Take ownership"
msgstr ""
msgid "PipelineSchedules|Target"
msgstr ""
+msgid "PipelineSchedules|Variables"
+msgstr ""
+
+msgid "PipelineSheduleIntervalPattern|Custom"
+msgstr ""
+
+msgid "Pipelines"
+msgstr ""
+
+msgid "Pipelines charts"
+msgstr ""
+
+msgid "Pipeline|all"
+msgstr ""
+
+msgid "Pipeline|success"
+msgstr ""
+
+msgid "Pipeline|with stage"
+msgstr ""
+
+msgid "Pipeline|with stages"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
+msgid "Project '%{project_name}' queued for deletion."
+msgstr ""
+
+msgid "Project '%{project_name}' was successfully created."
+msgstr ""
+
+msgid "Project '%{project_name}' was successfully updated."
+msgstr ""
+
+msgid "Project '%{project_name}' will be deleted."
+msgstr ""
+
+msgid "Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Project details"
+msgstr ""
+
+msgid "Project export could not be deleted."
+msgstr ""
+
+msgid "Project export has been deleted."
+msgstr ""
+
+msgid "Project export link has expired. Please generate a new export from your project settings."
+msgstr ""
+
+msgid "Project export started. A download link will be sent by email."
+msgstr ""
+
+msgid "Project home"
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
+msgid "ProjectFeature|Disabled"
+msgstr ""
+
+msgid "ProjectFeature|Everyone with access"
+msgstr ""
+
+msgid "ProjectFeature|Only team members"
+msgstr ""
+
+msgid "ProjectFileTree|Name"
+msgstr ""
+
+msgid "ProjectLastActivity|Never"
+msgstr ""
+
msgid "ProjectLifecycle|Stage"
msgstr "Phase"
+msgid "ProjectNetworkGraph|Graph"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Mehr"
+msgid "Readme"
+msgstr ""
+
+msgid "RefSwitcher|Branches"
+msgstr ""
+
+msgid "RefSwitcher|Tags"
+msgstr ""
+
msgid "Related Commits"
msgstr "Zugehörige Commits"
@@ -201,44 +869,142 @@ msgstr "Zugehörige Merge Requests"
msgid "Related Merged Requests"
msgstr "Zugehörige abgeschlossene Merge Requests"
+msgid "Remind later"
+msgstr ""
+
+msgid "Remove project"
+msgstr ""
+
+msgid "Repository"
+msgstr ""
+
+msgid "Request Access"
+msgstr ""
+
+msgid "Reset git storage health information"
+msgstr ""
+
+msgid "Reset health check access token"
+msgstr ""
+
+msgid "Reset runners registration token"
+msgstr ""
+
+msgid "Revert this commit"
+msgstr ""
+
+msgid "Revert this merge request"
+msgstr ""
+
msgid "Save pipeline schedule"
msgstr ""
msgid "Schedule a new pipeline"
msgstr ""
+msgid "Scheduling Pipelines"
+msgstr ""
+
+msgid "Search branches and tags"
+msgstr ""
+
+msgid "Select Archive Format"
+msgstr ""
+
msgid "Select a timezone"
msgstr ""
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr ""
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr ""
+
+msgid "Set up CI"
+msgstr ""
+
+msgid "Set up Koding"
+msgstr ""
+
+msgid "Set up auto deploy"
+msgstr ""
+
+msgid "SetPasswordToCloneLink|set a password"
+msgstr ""
+
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "Zeige %d Ereignis"
msgstr[1] "Zeige %d Ereignisse"
+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 ""
+
+msgid "Tag"
+msgid_plural "Tags"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Tags"
+msgstr ""
+
msgid "Target Branch"
msgstr ""
+msgid "Team"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "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."
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 ""
+
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."
msgid "The phase of the development lifecycle."
msgstr "Die Phase im Entwicklungsprozess."
+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 "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."
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."
+msgid "The project can be accessed by any logged in user."
+msgstr ""
+
+msgid "The project can be accessed without any authentication."
+msgstr ""
+
+msgid "The repository for this project does not exist."
+msgstr ""
+
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr "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."
@@ -254,6 +1020,12 @@ 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 ""
+
+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 "Zeit bis ein Issue geplant wird"
@@ -266,6 +1038,129 @@ msgstr "Zeit zwischen Anlegen und Mergen/Schließen eines Merge Requests"
msgid "Time until first merge request"
msgstr "Zeit bis zum ersten Merge Request"
+msgid "Timeago|%s days ago"
+msgstr ""
+
+msgid "Timeago|%s days remaining"
+msgstr ""
+
+msgid "Timeago|%s hours remaining"
+msgstr ""
+
+msgid "Timeago|%s minutes ago"
+msgstr ""
+
+msgid "Timeago|%s minutes remaining"
+msgstr ""
+
+msgid "Timeago|%s months ago"
+msgstr ""
+
+msgid "Timeago|%s months remaining"
+msgstr ""
+
+msgid "Timeago|%s seconds remaining"
+msgstr ""
+
+msgid "Timeago|%s weeks ago"
+msgstr ""
+
+msgid "Timeago|%s weeks remaining"
+msgstr ""
+
+msgid "Timeago|%s years ago"
+msgstr ""
+
+msgid "Timeago|%s years remaining"
+msgstr ""
+
+msgid "Timeago|1 day remaining"
+msgstr ""
+
+msgid "Timeago|1 hour remaining"
+msgstr ""
+
+msgid "Timeago|1 minute remaining"
+msgstr ""
+
+msgid "Timeago|1 month remaining"
+msgstr ""
+
+msgid "Timeago|1 week remaining"
+msgstr ""
+
+msgid "Timeago|1 year remaining"
+msgstr ""
+
+msgid "Timeago|Past due"
+msgstr ""
+
+msgid "Timeago|a day ago"
+msgstr ""
+
+msgid "Timeago|a month ago"
+msgstr ""
+
+msgid "Timeago|a week ago"
+msgstr ""
+
+msgid "Timeago|a while"
+msgstr ""
+
+msgid "Timeago|a year ago"
+msgstr ""
+
+msgid "Timeago|about %s hours ago"
+msgstr ""
+
+msgid "Timeago|about a minute ago"
+msgstr ""
+
+msgid "Timeago|about an hour ago"
+msgstr ""
+
+msgid "Timeago|in %s days"
+msgstr ""
+
+msgid "Timeago|in %s hours"
+msgstr ""
+
+msgid "Timeago|in %s minutes"
+msgstr ""
+
+msgid "Timeago|in %s months"
+msgstr ""
+
+msgid "Timeago|in %s seconds"
+msgstr ""
+
+msgid "Timeago|in %s weeks"
+msgstr ""
+
+msgid "Timeago|in %s years"
+msgstr ""
+
+msgid "Timeago|in 1 day"
+msgstr ""
+
+msgid "Timeago|in 1 hour"
+msgstr ""
+
+msgid "Timeago|in 1 minute"
+msgstr ""
+
+msgid "Timeago|in 1 month"
+msgstr ""
+
+msgid "Timeago|in 1 week"
+msgstr ""
+
+msgid "Timeago|in 1 year"
+msgstr ""
+
+msgid "Timeago|less than a minute ago"
+msgstr ""
+
msgid "Time|hr"
msgid_plural "Time|hrs"
msgstr[0] "h"
@@ -285,19 +1180,108 @@ msgstr "Gesamtzeit"
msgid "Total test time for all commits/merges"
msgstr "Gesamte Testlaufzeit für alle Commits/Merges"
+msgid "Unstar"
+msgstr ""
+
+msgid "Upload New File"
+msgstr ""
+
+msgid "Upload file"
+msgstr ""
+
+msgid "UploadLink|click to upload"
+msgstr ""
+
+msgid "Use the following registration token during setup:"
+msgstr ""
+
+msgid "Use your global notification setting"
+msgstr ""
+
+msgid "View open merge request"
+msgstr ""
+
+msgid "VisibilityLevel|Internal"
+msgstr ""
+
+msgid "VisibilityLevel|Private"
+msgstr ""
+
+msgid "VisibilityLevel|Public"
+msgstr ""
+
+msgid "VisibilityLevel|Unknown"
+msgstr ""
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Um diese Daten einsehen zu können, wenden Sie sich bitte an Ihren Administrator."
msgid "We don't have enough data to show this stage."
msgstr "Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen."
+msgid "Withdraw Access Request"
+msgstr ""
+
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You can only add files when you are on a branch"
+msgstr ""
+
msgid "You have reached your project limit"
msgstr ""
+msgid "You must sign in to star a project"
+msgstr ""
+
msgid "You need permission."
msgstr "Sie benötigen Zugriffsrechte."
+msgid "You will not get any notifications via email"
+msgstr ""
+
+msgid "You will only receive notifications for the events you choose"
+msgstr ""
+
+msgid "You will only receive notifications for threads you have participated in"
+msgstr ""
+
+msgid "You will receive notifications for any activity"
+msgstr ""
+
+msgid "You will receive notifications only for comments in which you were @mentioned"
+msgstr ""
+
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr ""
+
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr ""
+
+msgid "Your name"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] "Tag"
msgstr[1] "Tage"
+
+msgid "new merge request"
+msgstr ""
+
+msgid "notification emails"
+msgstr ""
+
+msgid "parent"
+msgid_plural "parents"
+msgstr[0] ""
+msgstr[1] "" \ No newline at end of file
diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po
index 0ac591d4927..84232be601e 100644
--- a/locale/en/gitlab.po
+++ b/locale/en/gitlab.po
@@ -82,6 +82,9 @@ msgstr ""
msgid "Add new directory"
msgstr ""
+msgid "All"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr ""
@@ -222,6 +225,9 @@ msgstr ""
msgid "CiStatus|running"
msgstr ""
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] ""
@@ -394,6 +400,24 @@ msgstr ""
msgid "Edit Pipeline Schedule %{id}"
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 ""
@@ -489,6 +513,9 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
+msgid "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr ""
@@ -518,6 +545,12 @@ msgstr ""
msgid "Last commit"
msgstr ""
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr ""
@@ -538,6 +571,9 @@ msgstr[1] ""
msgid "Median"
msgstr ""
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr ""
@@ -741,6 +777,9 @@ msgstr ""
msgid "Pipeline|with stages"
msgstr ""
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr ""
@@ -774,6 +813,9 @@ msgstr ""
msgid "Project home"
msgstr ""
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr ""
@@ -795,6 +837,9 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph"
msgstr ""
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr ""
@@ -925,6 +970,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 ""
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index 94ae131186b..4617de25a7c 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 09:53-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,9 @@ msgstr "Aro da diagramoj pri la seninterrompa integrado"
msgid "About auto deploy"
msgstr "Pri la aÅ­tomata disponigado"
+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 "Aktiva"
@@ -56,19 +76,32 @@ 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 "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}"
@@ -77,14 +110,8 @@ 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"
@@ -116,6 +143,9 @@ msgstr "Agordoj de seninterrompa integrado"
msgid "Cancel"
msgstr "Nuligi"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Elekti en branĉon"
@@ -194,6 +224,9 @@ msgstr "transsaltita"
msgid "CiStatus|running"
msgstr "plenumiÄanta"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "Enmetado"
@@ -241,12 +274,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 +310,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"
@@ -332,9 +354,15 @@ msgstr[1] "Disponigadoj"
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 +399,24 @@ msgstr "Redakti"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Redakti ĉenstablan planon %{id}"
+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 +462,13 @@ 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 "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,6 +476,24 @@ msgstr "Al via disbranĉigo"
msgid "GoToYourFork|Fork"
msgstr "Disbranĉigo"
+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"
@@ -435,12 +503,18 @@ 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 "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "Taskoj po la lasta monato"
@@ -470,6 +544,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"
@@ -490,9 +570,15 @@ msgstr[1] "Limigita al montrado de ne pli ol %d eventoj"
msgid "Median"
msgstr "Mediano"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "aldonos SSH-Ålosilon"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nova problemo"
@@ -690,6 +776,9 @@ msgstr "kun etapo"
msgid "Pipeline|with stages"
msgstr "kun etapoj"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "La projekto „%{project_name}“ estis alvicigita por forigado."
@@ -705,27 +794,27 @@ 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 "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "MalÅaltita"
@@ -747,6 +836,9 @@ msgstr "Etapo"
msgid "ProjectNetworkGraph|Graph"
msgstr "Grafeo"
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Legu pli"
@@ -783,9 +875,21 @@ 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"
@@ -810,13 +914,14 @@ 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}."
-msgstr ""
-"Kreu pasvorton por via konto por ebligi al vi eltiri kaj alpuÅi per "
-"%{protocol}."
+msgstr "Kreu pasvorton por via konto por ebligi al vi eltiri kaj alpuÅi per %{protocol}."
msgid "Set up CI"
msgstr "Agordi SI"
@@ -838,12 +943,18 @@ msgstr[1] "Estas montrataj %d eventoj"
msgid "Source code"
msgstr "Kodo"
+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 +969,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 +1005,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 +1192,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,8 +1214,7 @@ 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."
@@ -1174,12 +1231,8 @@ msgstr "Vi forigos „%{project_name_with_namespace}“. Oni NE POVAS malfari la
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"
@@ -1199,31 +1252,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"
@@ -1242,5 +1284,4 @@ msgstr "sciigoj per retpoÅto"
msgid "parent"
msgid_plural "parents"
msgstr[0] "patro"
-msgstr[1] "patroj"
-
+msgstr[1] "patroj" \ No newline at end of file
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index e43fd5fea15..8158bd275bd 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 09:37-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,9 @@ msgstr "Una colección de gráficos sobre Integración Continua"
msgid "About auto deploy"
msgstr "Acerca del auto despliegue"
+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 "Activo"
@@ -62,12 +81,27 @@ 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 "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}"
@@ -109,6 +143,9 @@ msgstr "Configuración de CI"
msgid "Cancel"
msgstr "Cancelar"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Escoger en la rama"
@@ -122,7 +159,7 @@ msgid "ChangeTypeAction|Revert"
msgstr "Revertir"
msgid "Changelog"
-msgstr "Changelog"
+msgstr ""
msgid "Charts"
msgstr "Gráficos"
@@ -187,6 +224,9 @@ msgstr "omitido"
msgid "CiStatus|running"
msgstr "en ejecución"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "Cambio"
@@ -234,6 +274,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 +314,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."
@@ -311,9 +354,15 @@ msgstr[1] "Despliegues"
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 +399,24 @@ msgstr "Editar"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Editar Programación del Pipeline %{id}"
+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,12 +464,36 @@ 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 "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 "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"
@@ -412,12 +503,18 @@ 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 "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "Trabajos del mes pasado"
@@ -447,6 +544,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"
@@ -467,9 +570,15 @@ msgstr[1] "Limitado a mostrar máximo %d eventos"
msgid "Median"
msgstr "Mediana"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "agregar una clave SSH"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nueva incidencia"
@@ -581,7 +690,7 @@ msgid "Owner"
msgstr "Propietario"
msgid "Pipeline"
-msgstr "Pipeline"
+msgstr ""
msgid "Pipeline Health"
msgstr "Estado del Pipeline"
@@ -650,7 +759,7 @@ msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "Personalizado"
msgid "Pipelines"
-msgstr "Pipelines"
+msgstr ""
msgid "Pipelines charts"
msgstr "Gráficos de los pipelines"
@@ -667,6 +776,9 @@ msgstr "con etapa"
msgid "Pipeline|with stages"
msgstr "con etapas"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Proyecto ‘%{project_name}’ en cola para eliminación."
@@ -682,6 +794,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 +812,9 @@ msgstr "Se inició la exportación del proyecto. Se enviará un enlace de descar
msgid "Project home"
msgstr "Inicio del proyecto"
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "Deshabilitada"
@@ -718,6 +836,9 @@ msgstr "Etapa"
msgid "ProjectNetworkGraph|Graph"
msgstr "Historial gráfico"
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Leer más"
@@ -754,9 +875,21 @@ 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"
@@ -781,6 +914,9 @@ 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"
@@ -807,12 +943,18 @@ msgstr[1] "Mostrando %d eventos"
msgid "Source code"
msgstr "Código fuente"
+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 +969,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 +1020,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 +1192,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"
@@ -1133,4 +1284,4 @@ msgstr "correos electrónicos de notificación"
msgid "parent"
msgid_plural "parents"
msgstr[0] "padre"
-msgstr[1] "padres"
+msgstr[1] "padres" \ No newline at end of file
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index 83f31f7a3b2..3daff3f5c19 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 09:53-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,9 @@ msgstr "Un ensemble de graphiques concernant l’Intégration Continue (CI)"
msgid "About auto deploy"
msgstr "A propos de l'auto-déploiement"
+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 "Actif"
@@ -61,36 +76,42 @@ 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 "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 "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 +120,7 @@ msgid "BranchSwitcherTitle|Switch branch"
msgstr "Changer de branche"
msgid "Branches"
-msgstr "Branches"
+msgstr ""
msgid "Browse Directory"
msgstr "Parcourir le dossier"
@@ -122,6 +143,9 @@ 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"
@@ -200,6 +224,9 @@ msgstr "ignoré"
msgid "CiStatus|running"
msgstr "en cours"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "Validation"
@@ -247,12 +274,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 +310,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"
@@ -337,11 +352,17 @@ msgstr[0] "Déploiement"
msgstr[1] "Déploiements"
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 +399,24 @@ msgstr "Éditer"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Éditer le pipeline programmé %{id}"
+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)"
@@ -423,8 +462,13 @@ 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 "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,6 +476,24 @@ msgstr "Aller à votre fourche"
msgid "GoToYourFork|Fork"
msgstr "Fourche"
+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"
@@ -441,12 +503,18 @@ 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 "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "Tâches pour le mois dernier"
@@ -476,6 +544,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"
@@ -496,9 +570,15 @@ msgstr[1] "Limiter l'affichage au plus à %d évènements"
msgid "Median"
msgstr "Médian"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "ajouter une clef SSH"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nouvel incident"
@@ -604,13 +684,13 @@ msgid "OpenedNDaysAgo|Opened"
msgstr "Ouvert"
msgid "Options"
-msgstr "Options"
+msgstr ""
msgid "Owner"
msgstr "Propriétaire"
msgid "Pipeline"
-msgstr "Pipeline"
+msgstr ""
msgid "Pipeline Health"
msgstr "Santé du Pipeline"
@@ -679,7 +759,7 @@ msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "Personnalisé"
msgid "Pipelines"
-msgstr "Pipelines"
+msgstr ""
msgid "Pipelines charts"
msgstr "Graphique des pipelines"
@@ -696,6 +776,9 @@ msgstr "avec l'étape"
msgid "Pipeline|with stages"
msgstr "avec les étapes"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Projet '%{project_name}' en attente de suppression."
@@ -709,8 +792,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 +803,18 @@ 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 "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "Désactivé"
@@ -754,6 +836,9 @@ msgstr "Étape"
msgid "ProjectNetworkGraph|Graph"
msgstr "Graphique "
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Lire plus"
@@ -790,9 +875,21 @@ 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"
@@ -817,13 +914,14 @@ 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}."
-msgstr ""
-"Définissez un mot de passe pour votre compte pour pouvoir tirer ou pousser "
-"par %{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)"
@@ -845,12 +943,18 @@ msgstr[1] "Affichage de %d évènements"
msgid "Source code"
msgstr "Code source"
+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 +969,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 +1005,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 +1192,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,9 +1214,7 @@ 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."
@@ -1181,19 +1228,11 @@ msgstr "Vous êtes sur le point de supprimer %{group_name}. Les groupes supprimÃ
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"
@@ -1211,39 +1250,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"
@@ -1261,6 +1283,5 @@ msgstr "courriels de notification"
msgid "parent"
msgid_plural "parents"
-msgstr[0] "parent"
-msgstr[1] "parents"
-
+msgstr[0] ""
+msgstr[1] "" \ No newline at end of file
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5a1db208d5a..97bc3d80642 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-08-24 09:29+0200\n"
-"PO-Revision-Date: 2017-08-24 09:29+0200\n"
+"POT-Creation-Date: 2017-08-31 17:34+0530\n"
+"PO-Revision-Date: 2017-08-31 17:34+0530\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -428,6 +427,9 @@ msgstr ""
msgid "Every week (Sundays at 4:00am)"
msgstr ""
+msgid "Explore projects"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr ""
@@ -838,6 +840,27 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph"
msgstr ""
+msgid "ProjectsDropdown|Frequently visited"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|No projects matched your query"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr ""
@@ -951,6 +974,9 @@ msgstr ""
msgid "StarProject|Star"
msgstr ""
+msgid "Starred projects"
+msgstr ""
+
msgid "Start a %{new_merge_request} with these changes"
msgstr ""
@@ -1272,6 +1298,9 @@ msgstr ""
msgid "Your name"
msgstr ""
+msgid "Your projects"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] ""
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index e719a3988e3..7b8bea46e26 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 10:25-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,9 @@ msgstr "Un insieme di grafici riguardo la Continuous Integration"
msgid "About auto deploy"
msgstr "Riguardo il rilascio automatico"
+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 "Attivo"
@@ -60,36 +76,42 @@ 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 "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 "Attach a file by drag &amp; drop or %{upload_link}"
+msgid "Are you sure you want to discard your changes?"
msgstr ""
-"Aggiungi un file tramite trascina &amp; rilascia ( drag &amp; drop) o "
-"%{upload_link}"
+
+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 "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 +120,7 @@ msgid "BranchSwitcherTitle|Switch branch"
msgstr "Cambia branch"
msgid "Branches"
-msgstr "Branches"
+msgstr ""
msgid "Browse Directory"
msgstr "Naviga direttori"
@@ -121,6 +143,9 @@ msgstr "Configurazione CI (Integrazione Continua)"
msgid "Cancel"
msgstr "Cancella"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Preleva nella branch"
@@ -134,13 +159,13 @@ msgid "ChangeTypeAction|Revert"
msgstr "Ripristina"
msgid "Changelog"
-msgstr "Changelog"
+msgstr ""
msgid "Charts"
msgstr "Grafici"
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 +224,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 +245,7 @@ msgid "CommitMessage|Add %{file_name}"
msgstr "Aggiungi %{file_name}"
msgid "Commits"
-msgstr "Commits"
+msgstr ""
msgid "Commits feed"
msgstr "Feed dei Commits"
@@ -246,12 +274,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 +302,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 +310,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"
@@ -337,9 +354,15 @@ msgstr[1] "Rilasci"
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 +399,24 @@ msgstr "Modifica"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Cambia programmazione della pipeline %{id}"
+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 +433,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 +452,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 +462,13 @@ 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 "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 +476,26 @@ msgstr "Vai il tuo fork"
msgid "GoToYourFork|Fork"
msgstr "Fork"
+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 "Housekeeping successfully started"
msgstr "Housekeeping iniziato con successo"
@@ -440,12 +503,18 @@ 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 "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "Jobs dell'ultimo mese"
@@ -475,6 +544,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"
@@ -495,9 +570,15 @@ msgstr[1] "Limita visualizzazione %d di eventi"
msgid "Median"
msgstr "Mediano"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "aggiungi una chiave SSH"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nuovo Issue"
@@ -606,10 +687,10 @@ msgid "Options"
msgstr "Opzioni"
msgid "Owner"
-msgstr "Owner"
+msgstr ""
msgid "Pipeline"
-msgstr "Pipeline"
+msgstr ""
msgid "Pipeline Health"
msgstr "Stato della Pipeline"
@@ -695,6 +776,9 @@ msgstr "con stadio"
msgid "Pipeline|with stages"
msgstr "con più stadi"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Il Progetto '%{project_name}' in coda di eliminazione."
@@ -710,27 +794,27 @@ 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 "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "Disabilitato"
@@ -752,6 +836,9 @@ msgstr "Stadio"
msgid "ProjectNetworkGraph|Graph"
msgstr "Grafico"
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Vedi altro"
@@ -788,9 +875,21 @@ 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"
@@ -815,13 +914,14 @@ 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}."
-msgstr ""
-"Establezca una contraseña en su cuenta para actualizar o enviar a través de "
-"%{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"
@@ -843,34 +943,37 @@ msgstr[1] "Visualizza %d eventi"
msgid "Source code"
msgstr "Codice Sorgente"
+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 +981,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 +1181,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 +1192,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,8 +1214,7 @@ 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"
@@ -1175,19 +1228,11 @@ msgstr "Stai per rimuovere il gruppo %{group_name}. I gruppi rimossi NON POSSONO
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"
@@ -1207,31 +1252,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"
@@ -1249,6 +1283,5 @@ msgstr "Notifiche via email"
msgid "parent"
msgid_plural "parents"
-msgstr[0] "parent"
-msgstr[1] "parents"
-
+msgstr[0] ""
+msgstr[1] "" \ No newline at end of file
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index bfa97aa21d7..670ac2d9684 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 10:14-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,9 @@ msgstr "CIã«ã¤ã„ã¦ã®ã‚°ãƒ©ãƒ•"
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 "有効"
@@ -63,12 +77,27 @@ msgstr "SSHã§ãƒ—ルやプッシュã™ã‚‹å ´åˆã¯ã€ãƒ—ロフィールã«SSHéµ
msgid "Add new directory"
msgstr "æ–°è¦ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’追加"
+msgid "All"
+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} ã§ãƒ•ã‚¡ã‚¤ãƒ«ã‚’添付"
@@ -76,13 +105,8 @@ 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 "ブランãƒã‚’検索"
@@ -114,6 +138,9 @@ msgstr "CI 設定"
msgid "Cancel"
msgstr "キャンセル"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "ピック先ブランãƒ:"
@@ -192,6 +219,9 @@ msgstr "スキップ済ã¿"
msgid "CiStatus|running"
msgstr "実行中"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "コミット"
@@ -238,9 +268,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 +304,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 "コード"
@@ -325,9 +347,15 @@ msgstr[0] "デプロイ"
msgid "Description"
msgstr "説明"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "ディレクトリå"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "次回ã‹ã‚‰è¡¨ç¤ºã—ãªã„"
@@ -364,6 +392,24 @@ msgstr "編集"
msgid "Edit Pipeline Schedule %{id}"
msgstr "パイプラインスケジュール %{id} を編集"
+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,12 +456,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 "ホーム"
@@ -425,12 +495,18 @@ msgstr "ãƒã‚¦ã‚¹ã‚­ãƒ¼ãƒ”ングã¯æ­£å¸¸ã«èµ·å‹•ã—ã¾ã—ãŸã€‚"
msgid "Import repository"
msgstr "レãƒã‚¸ãƒˆãƒªãƒ¼ã‚’インãƒãƒ¼ãƒˆ"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "é–“éš”ã®ãƒ‘ターン"
msgid "Introducing Cycle Analytics"
msgstr "サイクル分æžã®ã”紹介"
+msgid "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "先月ã®ã‚¸ãƒ§ãƒ–"
@@ -459,6 +535,12 @@ msgstr "最新アップデート"
msgid "Last commit"
msgstr "最新コミット"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "詳ã—ã見る:"
@@ -478,9 +560,15 @@ msgstr[0] "イベント表示数を最大 %d 個ã«åˆ¶é™"
msgid "Median"
msgstr "中央値"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "SSH éµã‚’追加"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "æ–°è¦èª²é¡Œ"
@@ -677,6 +765,9 @@ msgstr "ステージã‚ã‚Š"
msgid "Pipeline|with stages"
msgstr "ステージã‚ã‚Š"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "'%{project_name}' プロジェクトã¯å‰Šé™¤å‡¦ç†å¾…ã¡ã§ã™ã€‚"
@@ -692,15 +783,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 +801,9 @@ msgstr "プロジェクトã®ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã‚’開始ã—ã¾ã—ãŸã€‚ダウン
msgid "Project home"
msgstr "プロジェクトホーム"
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "無効"
@@ -730,11 +825,14 @@ msgstr "ステージ"
msgid "ProjectNetworkGraph|Graph"
msgstr "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‚°ãƒ©ãƒ•"
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "続ãを読む"
msgid "Readme"
-msgstr "Readme"
+msgstr ""
msgid "RefSwitcher|Branches"
msgstr "ブランãƒ"
@@ -766,9 +864,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 "ã“ã®ã‚³ãƒŸãƒƒãƒˆã‚’リãƒãƒ¼ãƒˆ"
@@ -793,6 +903,9 @@ msgstr "アーカイブã®ãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆã‚’é¸æŠž"
msgid "Select a timezone"
msgstr "タイムゾーンをé¸æŠž"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "ターゲットブランãƒã‚’é¸æŠž"
@@ -818,12 +931,18 @@ msgstr[0] "%d ã®ã‚¤ãƒ™ãƒ³ãƒˆã‚’表示中"
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 "ã“ã®å¤‰æ›´ã§ %{new_merge_request} を作æˆã™ã‚‹"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "ブランãƒãƒ»ã‚¿ã‚°åˆ‡ã‚Šæ›¿ãˆ"
@@ -837,12 +956,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 +968,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 +992,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 +1177,9 @@ msgstr "ファイルをアップロード"
msgid "UploadLink|click to upload"
msgstr "クリックã—ã¦ã‚¢ãƒƒãƒ—ロード"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "全体通知設定を利用"
@@ -1125,14 +1213,10 @@ msgstr "%{group_name} グループを削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚ 削除
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"
@@ -1153,28 +1237,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"
@@ -1192,4 +1267,4 @@ msgstr "メール通知"
msgid "parent"
msgid_plural "parents"
-msgstr[0] "親"
+msgstr[0] "親" \ No newline at end of file
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index 340c8955d20..df850115222 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -1,39 +1,51 @@
-# Korean 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.
-# 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 10:05-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-08 08:32-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"
"Plural-Forms: nplurals=1; plural=0;\n"
-"X-Generator: Zanata 3.9.6\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} ë‹˜ì´ ì»¤ë°‹í•˜ì˜€ìŠµë‹ˆë‹¤. "
+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 "지ì†ì ì¸ í†µí•©ì— ê´€í•œ 그래프 모ìŒ"
@@ -41,6 +53,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 "활성"
@@ -62,12 +77,27 @@ msgstr "í”„ë¡œí•„ì— SSH 키를 추가하여 SSH를 통해 Pull 하거나 Pushí•
msgid "Add new directory"
msgstr "새 디렉토리 추가"
+msgid "All"
+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}"
@@ -75,13 +105,8 @@ 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 "브랜치 검색"
@@ -113,6 +138,9 @@ msgstr "CI 설정"
msgid "Cancel"
msgstr "취소"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "브랜치ì—ì„œ Pick"
@@ -191,6 +219,9 @@ msgstr "건너 뜀"
msgid "CiStatus|running"
msgstr "실행 중"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "커밋"
@@ -237,9 +268,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"
@@ -272,20 +304,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"
@@ -322,9 +347,15 @@ msgstr[0] "ë°°í¬"
msgid "Description"
msgstr "설명"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "디렉토리 ì´ë¦„"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "다시 표시하지 ì•ŠìŒ"
@@ -361,6 +392,24 @@ msgstr "편집"
msgid "Edit Pipeline Schedule %{id}"
msgstr "파ì´í”„ë¼ì¸ 스케줄 편집 %{id}"
+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ì‹œì—)"
@@ -407,12 +456,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 "홈"
@@ -422,12 +495,18 @@ msgstr "Housekeepingì´ ì„±ê³µì ìœ¼ë¡œ 시작ë˜ì—ˆìŠµë‹ˆë‹¤"
msgid "Import repository"
msgstr "저장소 가져 오기"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "주기 패턴"
msgid "Introducing Cycle Analytics"
msgstr "Cycle Analytics 소개"
+msgid "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "지난달 Jobs"
@@ -456,6 +535,12 @@ msgstr "최근 ì—…ë°ì´íŠ¸:"
msgid "Last commit"
msgstr "최근 커밋"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "ë” ìžì„¸ížˆ 알아보기"
@@ -475,9 +560,15 @@ msgstr[0] "최대 %d ì´ë²¤íŠ¸ 만 표시하는 것으로 제한ë©ë‹ˆë‹¤."
msgid "Median"
msgstr "중앙값"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "SSH 키 추가"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "새 ì´ìŠˆ"
@@ -674,6 +765,9 @@ msgstr "스테ì´ì§•"
msgid "Pipeline|with stages"
msgstr "스테ì´ì§•"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "'%{project_name}'프로ì íŠ¸ê°€ ì‚­ì œ 처리 중입니다."
@@ -689,15 +783,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."
@@ -706,6 +801,9 @@ msgstr "프로ì íŠ¸ 내보내기가 시작ë˜ì—ˆìŠµë‹ˆë‹¤. 다운로드 ë§í¬ë
msgid "Project home"
msgstr "프로ì íŠ¸ 홈"
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "사용 안 함"
@@ -727,11 +825,14 @@ msgstr "스테ì´ì§•"
msgid "ProjectNetworkGraph|Graph"
msgstr "그래프"
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "ë” ì½ê¸°"
msgid "Readme"
-msgstr "Readme"
+msgstr ""
msgid "RefSwitcher|Branches"
msgstr "브랜치"
@@ -763,9 +864,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 "ì´ ì»¤ë°‹ ë˜ëŒë¦¬ê¸°"
@@ -790,6 +903,9 @@ msgstr "ì•„ì¹´ì´ë¸Œ í¬ë§· ì„ íƒ"
msgid "Select a timezone"
msgstr "시간대 ì„ íƒ"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "ëŒ€ìƒ ë¸Œëžœì¹˜ ì„ íƒ"
@@ -815,12 +931,18 @@ msgstr[0] "%d ê°œì˜ ì´ë²¤íŠ¸ 표시 중"
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 "ì´ ë³€ê²½ 사항으로 %{new_merge_request} ì„ ì‹œìž‘í•˜ì‹­ì‹œì˜¤."
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "스위치 브랜치/태그"
@@ -834,13 +956,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 "해당 단계ì—ì„œ 수집 ëœ ë°ì´í„°ê°€ ì´ë²¤íŠ¸ 모ìŒì— 추가ë˜ì—ˆìŠµë‹ˆë‹¤."
@@ -848,38 +968,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 "ì´ í”„ë¡œì íŠ¸ëŠ” ë¡œê·¸ì¸ í•œ 사용ìžê°€ë§Œ 액세스 í•  수 있습니다."
@@ -890,44 +992,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."
+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 "즉, 빈 저장소를 만들거나 기존 저장소를 가져올 때까지 코드를 Push 할 수 없습니다."
msgid "Time before an issue gets scheduled"
@@ -1094,6 +1177,9 @@ msgstr "íŒŒì¼ ì—…ë¡œë“œ"
msgid "UploadLink|click to upload"
msgstr "업로드하려면 í´ë¦­í•˜ì‹­ì‹œì˜¤."
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "전체 알림 설정 사용"
@@ -1125,16 +1211,12 @@ msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored!
msgstr "%{group_name} ê·¸ë£¹ì„ ì œê±°í•˜ë ¤ê³ í•©ë‹ˆë‹¤. \"ì •ë§ë¡œ\" 확실합니까?"
msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
-msgstr "%{project_name_with_namespace} 프로ì íŠ¸ë¥¼ 삭제하려고합니다. "ì‚­ì œëœ í”„ë¡œì íŠ¸ë¥¼ ë³µì› í•  수 없습니다! \"ì •ë§ë¡œ\" 확실합니까?"
+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"
@@ -1155,31 +1237,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 "ê·€í•˜ì˜ ì´ë¦„"
@@ -1196,5 +1267,4 @@ msgstr "알림 ì´ë©”ì¼"
msgid "parent"
msgid_plural "parents"
-msgstr[0] "부모"
-
+msgstr[0] "부모" \ No newline at end of file
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index a2df8ea549c..d8887110867 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 10:14-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,9 @@ msgstr "Uma coleção de gráficos sobre Integração Contínua"
msgid "About auto deploy"
msgstr "Sobre o deploy automático"
+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 "Ativo"
@@ -65,28 +81,37 @@ 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 "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 "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 +120,7 @@ msgid "BranchSwitcherTitle|Switch branch"
msgstr "Mudar de branch"
msgid "Branches"
-msgstr "Branches"
+msgstr ""
msgid "Browse Directory"
msgstr "Navegar no Diretório"
@@ -118,6 +143,9 @@ msgstr "Configuração da IC"
msgid "Cancel"
msgstr "Cancelar"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Pick para um branch"
@@ -196,10 +224,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 +245,7 @@ msgid "CommitMessage|Add %{file_name}"
msgstr "Adicionar %{file_name}"
msgid "Commits"
-msgstr "Commits"
+msgstr ""
msgid "Commits feed"
msgstr "Feed de commits"
@@ -243,12 +274,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 +310,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"
@@ -335,9 +354,15 @@ msgstr[1] "Implantações"
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 +399,24 @@ msgstr "Alterar"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Alterar Agendamento do Pipeline %{id}"
+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 +452,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,12 +464,36 @@ 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 "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 "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"
@@ -436,12 +503,18 @@ 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 "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "Jobs no último mês"
@@ -471,6 +544,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"
@@ -491,9 +570,15 @@ msgstr[1] "Limitado a mostrar %d eventos, no máximo"
msgid "Median"
msgstr "Mediana"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "adicione uma chave SSH"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Nova Issue"
@@ -605,7 +690,7 @@ msgid "Owner"
msgstr "Proprietário"
msgid "Pipeline"
-msgstr "Pipeline"
+msgstr ""
msgid "Pipeline Health"
msgstr "Saúde da Pipeline"
@@ -674,7 +759,7 @@ msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "Personalizado"
msgid "Pipelines"
-msgstr "Pipelines"
+msgstr ""
msgid "Pipelines charts"
msgstr "Gráficos de pipelines"
@@ -691,6 +776,9 @@ msgstr "com etapa"
msgid "Pipeline|with stages"
msgstr "com etapas"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Projeto'%{project_name}' marcado para exclusão."
@@ -704,8 +792,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 +803,18 @@ 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 "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "Desabilitado"
@@ -749,6 +836,9 @@ msgstr "Etapa"
msgid "ProjectNetworkGraph|Graph"
msgstr "Ãrvore"
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Leia mais"
@@ -785,9 +875,21 @@ 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"
@@ -812,13 +914,14 @@ 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}."
-msgstr ""
-"Defina uma senha para sua conta para aceitar ou entregar código via "
-"%{protocol}."
+msgstr "Defina uma senha para sua conta para aceitar ou entregar código via %{protocol}."
msgid "Set up CI"
msgstr "Configurar CI"
@@ -840,34 +943,37 @@ msgstr[1] "Mostrando %d eventos"
msgid "Source code"
msgstr "Código-fonte"
+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 +981,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 +1005,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 +1033,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 +1192,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"
@@ -1164,19 +1222,17 @@ msgstr "Esta etapa não possui dados suficientes para exibição."
msgid "Withdraw Access Request"
msgstr "Remover Requisição de Acesso"
-
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}. 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 transfer %{project_name_with_namespace} to another owner. "
-"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ê 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"
@@ -1196,31 +1252,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"
@@ -1239,5 +1284,4 @@ msgstr "emails de notificação"
msgid "parent"
msgid_plural "parents"
msgstr[0] "pai"
-msgstr[1] "pais"
-
+msgstr[1] "pais" \ No newline at end of file
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index 6661232850a..926995d1f91 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 09:41-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,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 "Ðктивный"
@@ -66,19 +80,32 @@ 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 "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}"
@@ -88,14 +115,8 @@ 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 "ПоиÑк веток"
@@ -127,6 +148,9 @@ msgstr "ÐаÑтройка CI"
msgid "Cancel"
msgstr "Отмена"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Выбрать в ветке"
@@ -205,6 +229,9 @@ msgstr "пропущено"
msgid "CiStatus|running"
msgstr "выполнÑетÑÑ"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "Коммит"
@@ -253,12 +280,11 @@ 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 ""
-"Создать личный токен на аккаунте Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ отправки через "
-"%{protocol}."
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr "Создать личный токен на аккаунте Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ отправки через %{protocol}."
msgid "Create directory"
msgstr "Создать директорию"
@@ -290,25 +316,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 "ÐапиÑание кода"
@@ -346,9 +361,15 @@ msgstr[2] "Размещение"
msgid "Description"
msgstr "ОпиÑание"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "Каталог"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "Ðе показывать Ñнова"
@@ -385,6 +406,24 @@ msgstr "Редактировать"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Изменить раÑпиÑание конвейера %{id}"
+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,12 +472,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 "ГлавнаÑ"
@@ -448,12 +511,18 @@ msgstr "ОчиÑтка уÑпешно запущена"
msgid "Import repository"
msgstr "Импорт репозиториÑ"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Шаблон интервала"
msgid "Introducing Cycle Analytics"
msgstr "Внедрение Цикла Ðналитик"
+msgid "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "Работы за прошлый меÑÑц"
@@ -484,6 +553,12 @@ msgstr "ПоÑледнее обновление"
msgid "Last commit"
msgstr "ПоÑледний коммит"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "Узнайте больше в"
@@ -505,9 +580,15 @@ msgstr[2] "Ограничение %d Ñобытий"
msgid "Median"
msgstr "Среднее"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "добавить ключ SSH"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Обращение"
@@ -706,6 +787,9 @@ msgstr "Ñо Ñтадией"
msgid "Pipeline|with stages"
msgstr "Ñо ÑтадиÑми"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Проект '%{project_name}' добавлен в очередь на удаление."
@@ -721,27 +805,27 @@ 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 "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "Отключено"
@@ -763,11 +847,14 @@ msgstr "Этап"
msgid "ProjectNetworkGraph|Graph"
msgstr "Граф"
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Подробнее"
msgid "Readme"
-msgstr "Readme"
+msgstr ""
msgid "RefSwitcher|Branches"
msgstr "Ветки"
@@ -799,9 +886,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 "Отменить Ñто изменение"
@@ -826,13 +925,14 @@ 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}."
-msgstr ""
-"УÑтановите пароль в Ñвоем аккаунте, чтобы отправлÑÑ‚ÑŒ или получать код через "
-"%{protocol}."
+msgstr "УÑтановите пароль в Ñвоем аккаунте, чтобы отправлÑÑ‚ÑŒ или получать код через %{protocol}."
msgid "Set up CI"
msgstr "ÐаÑтройка CI"
@@ -855,12 +955,18 @@ msgstr[2] "Показано %d Ñобытий"
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 "Ðачать %{new_merge_request} Ñ Ñтих изменений"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "Переключить ветка/тег"
@@ -876,14 +982,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 +994,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 +1018,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 +1207,9 @@ msgstr "Загрузить файл"
msgid "UploadLink|click to upload"
msgstr "кликните Ð´Ð»Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "ИÑпользуютÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ñ‹Ð¹ наÑтройки уведомлений"
@@ -1185,19 +1243,11 @@ msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить %{group_name}. Удаленн
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 "Ð’Ñ‹ можете добавлÑÑ‚ÑŒ только файлы, когда находитеÑÑŒ в ветке"
@@ -1217,34 +1267,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 "Ваше имÑ"
@@ -1265,5 +1301,4 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "иÑточник"
msgstr[1] "иÑточники"
-msgstr[2] "иÑточники"
-
+msgstr[2] "иÑточники" \ No newline at end of file
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 0ac0499e315..5f9f087ff64 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 09:49-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,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 "Ðктивний"
@@ -63,19 +80,32 @@ 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 "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}"
@@ -85,14 +115,8 @@ 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 "Пошук гілок"
@@ -124,6 +148,9 @@ msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI"
msgid "Cancel"
msgstr "СкаÑувати"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "Вибрати в гілці"
@@ -202,6 +229,9 @@ msgstr "пропущено"
msgid "CiStatus|running"
msgstr "виконуєтьÑÑ"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "Комміт"
@@ -250,12 +280,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 +316,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 "Код"
@@ -342,9 +361,15 @@ msgstr[2] "Розгортань"
msgid "Description"
msgstr "ОпиÑ"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "Ім'Ñ ÐºÐ°Ñ‚Ð°Ð»Ð¾Ð³Ñƒ"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "Ðе показувати знову"
@@ -381,6 +406,24 @@ msgstr "Редагувати"
msgid "Edit Pipeline Schedule %{id}"
msgstr "Редагувати Розклад Конвеєра %{id}"
+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,12 +472,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 "Головна"
@@ -444,12 +511,18 @@ msgstr "ÐžÑ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ ÑƒÑпішно розпочато"
msgid "Import repository"
msgstr "Імпорт репозеторіÑ"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "Шаблон інтервалу"
msgid "Introducing Cycle Analytics"
msgstr "ПредÑтавлÑємо аналітику циклу"
+msgid "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "КількіÑÑ‚ÑŒ завдань за оÑтанній міÑÑць"
@@ -480,6 +553,12 @@ msgstr "ОÑтаннє оновленнÑ"
msgid "Last commit"
msgstr "ОÑтанній комміт"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "ДізнайтеÑÑŒ більше"
@@ -501,9 +580,15 @@ msgstr[2] "ÐžÐ±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ %d подій"
msgid "Median"
msgstr "Медіана"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "не додаÑте SSH ключ"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Ðова проблема"
@@ -702,6 +787,9 @@ msgstr "зі Ñтадією"
msgid "Pipeline|with stages"
msgstr "зі ÑтадіÑми"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "Проект '%{project_name}' доданий в чергу на видаленнÑ."
@@ -717,27 +805,27 @@ 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 "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "Вимкнено"
@@ -759,6 +847,9 @@ msgstr "Етап"
msgid "ProjectNetworkGraph|Graph"
msgstr "ІÑторіÑ"
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "Докладніше"
@@ -795,9 +886,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 "СкаÑувати цей комміт"
@@ -822,13 +925,14 @@ 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}."
-msgstr ""
-"Ð’Ñтановіть пароль Ñвого облікового запиÑу, щоб відправлÑти або отримувати "
-"код через %{protocol}."
+msgstr "Ð’Ñтановіть пароль Ñвого облікового запиÑу, щоб відправлÑти або отримувати код через %{protocol}."
msgid "Set up CI"
msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI"
@@ -851,12 +955,18 @@ msgstr[2] "Показано %d подій"
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 "Почати %{new_merge_request} з цих змін"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "тег"
@@ -872,14 +982,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 +994,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 +1018,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 +1207,9 @@ msgstr "Завантажити файл"
msgid "UploadLink|click to upload"
msgstr "ÐатиÑніть, щоб завантажити"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "ВикориÑтовуютьÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ñ– Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ"
@@ -1179,19 +1243,11 @@ msgstr "Ви хочете видалити %{group_name}. Видалені грÑ
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 "Ви можете додавати тільки файли, коли перебуваєте в гілці"
@@ -1211,35 +1267,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 "Ваше ім'Ñ"
@@ -1260,5 +1301,4 @@ msgid "parent"
msgid_plural "parents"
msgstr[0] "джерело"
msgstr[1] "джерела"
-msgstr[2] "джерел"
-
+msgstr[2] "джерел" \ No newline at end of file
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index a3d0027212c..eb607acf1f4 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 09:59-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 ""
+
+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 "æŒç»­é›†æˆæ•°æ®å›¾"
@@ -38,6 +53,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 "å¯ç”¨"
@@ -59,12 +77,27 @@ msgstr "新建一个用于推é€æˆ–拉å–çš„ SSH 秘钥到账å·ä¸­ã€‚"
msgid "Add new directory"
msgstr "添加目录"
+msgid "All"
+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}"
@@ -72,13 +105,8 @@ 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 "æœç´¢åˆ†æ”¯"
@@ -110,6 +138,9 @@ msgstr "CI é…ç½®"
msgid "Cancel"
msgstr "å–消"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "选择分支"
@@ -188,6 +219,9 @@ msgstr "已跳过"
msgid "CiStatus|running"
msgstr "è¿è¡Œä¸­"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "æ交"
@@ -234,9 +268,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 +304,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,9 +347,15 @@ msgstr[0] "部署"
msgid "Description"
msgstr "æè¿°"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "目录å称"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "ä¸å†æ˜¾ç¤º"
@@ -357,6 +392,24 @@ msgstr "编辑"
msgid "Edit Pipeline Schedule %{id}"
msgstr "编辑 %{id} æµæ°´çº¿è®¡åˆ’"
+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,12 +456,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 "首页"
@@ -418,12 +495,18 @@ msgstr "已开始维护"
msgid "Import repository"
msgstr "导入存储库"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "循环周期"
msgid "Introducing Cycle Analytics"
msgstr "周期分æžç®€ä»‹"
+msgid "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "上个月的作业"
@@ -452,6 +535,12 @@ msgstr "最åŽæ›´æ–°"
msgid "Last commit"
msgstr "最åŽæ交"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "了解更多"
@@ -471,9 +560,15 @@ msgstr[0] "最多显示 %d 个事件"
msgid "Median"
msgstr "中ä½æ•°"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "新建 SSH 公钥"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新建议题"
@@ -670,6 +765,9 @@ msgstr "于阶段"
msgid "Pipeline|with stages"
msgstr "于阶段"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "项目 '%{project_name}' 已进入删除队列。"
@@ -685,15 +783,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 +801,9 @@ msgstr "项目导出已开始。下载链接将通过电å­é‚®ä»¶å‘é€ã€‚"
msgid "Project home"
msgstr "项目首页"
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "åœç”¨"
@@ -723,6 +825,9 @@ msgstr "阶段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支图"
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "了解更多"
@@ -759,9 +864,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 "还原此æ交"
@@ -786,6 +903,9 @@ msgstr "选择下载格å¼"
msgid "Select a timezone"
msgstr "选择时区"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "选择目标分支"
@@ -811,12 +931,18 @@ msgstr[0] "显示 %d 个事件"
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 "由此更改 %{new_merge_request}"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "切æ¢åˆ†æ”¯/标签"
@@ -830,10 +956,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 +968,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 +992,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 ""
+
+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 +1177,9 @@ msgstr "上传文件"
msgid "UploadLink|click to upload"
msgstr "点击上传"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "使用全局通知设置"
@@ -1107,14 +1213,10 @@ msgstr "å³å°†åˆ é™¤ %{group_name}。已删除的群组无法æ¢å¤ï¼ç¡®å®šç»§ç
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"
@@ -1135,26 +1237,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"
@@ -1172,4 +1267,4 @@ msgstr "通知邮件"
msgid "parent"
msgid_plural "parents"
-msgstr[0] "父级"
+msgstr[0] "父级" \ No newline at end of file
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index f4d33862a36..74c7b464091 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 09:59-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 ""
+
+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 "相關æŒçºŒé›†æˆçš„圖åƒé›†åˆ"
@@ -37,6 +53,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 "啟用"
@@ -58,12 +77,27 @@ msgstr "新增壹個用於推é€æˆ–拉å–çš„ SSH 秘鑰到賬號中。"
msgid "Add new directory"
msgstr "添加新目錄"
+msgid "All"
+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}"
@@ -71,13 +105,8 @@ 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 "æœç´¢åˆ†æ”¯"
@@ -109,6 +138,9 @@ msgstr "CI é…ç½®"
msgid "Cancel"
msgstr "å–消"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "挑é¸åˆ°åˆ†æ”¯"
@@ -187,6 +219,9 @@ msgstr "已跳éŽ"
msgid "CiStatus|running"
msgstr "é‹è¡Œä¸­"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "æ交"
@@ -233,9 +268,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"
@@ -268,19 +304,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"
@@ -317,9 +347,15 @@ msgstr[0] "部署"
msgid "Description"
msgstr "æè¿°"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "目錄å稱"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "ä¸å†é¡¯ç¤º"
@@ -356,6 +392,24 @@ msgstr "編輯"
msgid "Edit Pipeline Schedule %{id}"
msgstr "編輯 %{id} æµæ°´ç·šè¨ˆåŠƒ"
+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 點)"
@@ -402,12 +456,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 "首é "
@@ -417,12 +495,18 @@ msgstr "已開始維護"
msgid "Import repository"
msgstr "導入存儲庫"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "循環週期"
msgid "Introducing Cycle Analytics"
msgstr "週期分æžç°¡ä»‹"
+msgid "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "上個月的作業"
@@ -451,6 +535,12 @@ msgstr "最後更新"
msgid "Last commit"
msgstr "最後æ交"
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "了解更多"
@@ -470,9 +560,15 @@ msgstr[0] "最多顯示 %d 個事件"
msgid "Median"
msgstr "中ä½æ•¸"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "添加壹個 SSH 公鑰"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "新建議題"
@@ -669,6 +765,9 @@ msgstr "於階段"
msgid "Pipeline|with stages"
msgstr "於階段"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "項目 '%{project_name}' 已進入刪除隊列。"
@@ -684,15 +783,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 +801,9 @@ msgstr "項目導出已開始。下載éˆæŽ¥å°‡é€šéŽé›»å­éƒµä»¶ç™¼é€ã€‚"
msgid "Project home"
msgstr "項目首é "
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "åœç”¨"
@@ -722,6 +825,9 @@ msgstr "階段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支圖"
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "了解更多"
@@ -758,9 +864,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 "還原此æ交"
@@ -785,6 +903,9 @@ msgstr "é¸æ“‡ä¸‹è¼‰æ ¼å¼"
msgid "Select a timezone"
msgstr "é¸æ“‡æ™‚å€"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "é¸æ“‡ç›®æ¨™åˆ†æ”¯"
@@ -810,12 +931,18 @@ msgstr[0] "顯示 %d 個事件"
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 "由此更改 %{new_merge_request}"
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "切æ›åˆ†æ”¯/標籤"
@@ -829,10 +956,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 +968,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 +992,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 ""
+
+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 +1177,9 @@ msgstr "上傳文件"
msgid "UploadLink|click to upload"
msgstr "點擊上傳"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "使用全局通知設置"
@@ -1106,14 +1213,10 @@ msgstr "å³å°‡åˆªé™¤ %{group_name}。已刪除的群組無法æ¢å¾©ï¼ç¢ºå®šç¹¼ç
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"
@@ -1134,26 +1237,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"
@@ -1171,4 +1267,4 @@ msgstr "通知郵件"
msgid "parent"
msgid_plural "parents"
-msgstr[0] "父級"
+msgstr[0] "父級" \ No newline at end of file
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index 205d4712316..1fc6b79187f 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-08-18 14:15+0530\n"
+"PO-Revision-Date: 2017-08-23 09:59-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 ""
+
+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) 相關的圖表"
@@ -41,6 +53,9 @@ msgstr "æŒçºŒæ•´åˆ (CI) 相關的圖表"
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 "啟用"
@@ -62,12 +77,27 @@ msgstr "請先新增 SSH 金鑰到您的個人帳號,æ‰èƒ½ä½¿ç”¨ SSH 來上å‚
msgid "Add new directory"
msgstr "新增目錄"
+msgid "All"
+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 ""
+
+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}"
@@ -75,13 +105,8 @@ 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)"
@@ -113,6 +138,9 @@ msgstr "CI 組態"
msgid "Cancel"
msgstr "å–消"
+msgid "Cancel edit"
+msgstr ""
+
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "挑é¸åˆ°åˆ†æ”¯ (branch) "
@@ -191,6 +219,9 @@ msgstr "已跳éŽ"
msgid "CiStatus|running"
msgstr "執行中"
+msgid "Comments"
+msgstr ""
+
msgid "Commit"
msgid_plural "Commits"
msgstr[0] "更動記錄 (commit) "
@@ -237,9 +268,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 ""
+
+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 +304,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"
@@ -321,9 +347,15 @@ msgstr[0] "部署"
msgid "Description"
msgstr "æè¿°"
+msgid "Details"
+msgstr ""
+
msgid "Directory name"
msgstr "目錄å稱"
+msgid "Discard changes"
+msgstr ""
+
msgid "Don't show again"
msgstr "ä¸å†é¡¯ç¤º"
@@ -360,6 +392,24 @@ msgstr "編輯"
msgid "Edit Pipeline Schedule %{id}"
msgstr "編輯 %{id} æµæ°´ç·š (pipeline) 排程"
+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 "æ¯æ—¥åŸ·è¡Œï¼ˆæ·©æ™¨å››é»žï¼‰"
@@ -406,12 +456,36 @@ msgstr "從議題 (issue) 建立直到部署至營é‹ç’°å¢ƒ"
msgid "From merge request merge until deploy to production"
msgstr "從請求被åˆä½µå¾Œ (merge request merged) 直到部署至營é‹ç’°å¢ƒ"
+msgid "Git storage health information has been reset"
+msgstr ""
+
+msgid "GitLab Runner section"
+msgstr ""
+
msgid "Go to your fork"
msgstr "å‰å¾€æ‚¨çš„分支 (fork) "
msgid "GoToYourFork|Fork"
msgstr "å‰å¾€æ‚¨çš„分支 (fork) "
+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 "首é "
@@ -421,12 +495,18 @@ msgstr "已開始維護"
msgid "Import repository"
msgstr "匯入檔案庫 (repository)"
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
msgid "Interval Pattern"
msgstr "循環週期"
msgid "Introducing Cycle Analytics"
msgstr "週期分æžç°¡ä»‹"
+msgid "Issue events"
+msgstr ""
+
msgid "Jobs for last month"
msgstr "上個月的任務 (job) "
@@ -455,6 +535,12 @@ msgstr "最後更新"
msgid "Last commit"
msgstr "最後更動記錄 (commit) "
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
msgid "Learn more in the"
msgstr "了解更多"
@@ -474,9 +560,15 @@ msgstr[0] "é™åˆ¶æœ€å¤šé¡¯ç¤º %d 個事件"
msgid "Median"
msgstr "中ä½æ•¸"
+msgid "Merge events"
+msgstr ""
+
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "新增 SSH 金鑰"
+msgid "More information is available|here"
+msgstr ""
+
msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "建立議題 (issue) "
@@ -673,6 +765,9 @@ msgstr "於階段"
msgid "Pipeline|with stages"
msgstr "於階段"
+msgid "Project"
+msgstr ""
+
msgid "Project '%{project_name}' queued for deletion."
msgstr "專案 '%{project_name}' 已加入刪除佇列。"
@@ -688,15 +783,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 +801,9 @@ msgstr "專案導出已開始。完æˆå¾Œä¸‹è¼‰é€£çµæœƒé€åˆ°æ‚¨çš„信箱。"
msgid "Project home"
msgstr "專案首é "
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
msgid "ProjectFeature|Disabled"
msgstr "åœç”¨"
@@ -726,6 +825,9 @@ msgstr "階段"
msgid "ProjectNetworkGraph|Graph"
msgstr "分支圖"
+msgid "Push events"
+msgstr ""
+
msgid "Read more"
msgstr "瞭解更多"
@@ -762,9 +864,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 "還原此更動記錄 (commit)"
@@ -789,6 +903,9 @@ msgstr "é¸æ“‡ä¸‹è¼‰æ ¼å¼"
msgid "Select a timezone"
msgstr "é¸æ“‡æ™‚å€"
+msgid "Select existing branch"
+msgstr ""
+
msgid "Select target branch"
msgstr "é¸æ“‡ç›®æ¨™åˆ†æ”¯ (branch) "
@@ -814,12 +931,18 @@ msgstr[0] "顯示 %d 個事件"
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 "以這些改動建立一個新的 %{new_merge_request} "
+msgid "Start the Runner!"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr "切æ›åˆ†æ”¯ (branch) 或標籤"
@@ -833,12 +956,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."
+msgid "Team"
msgstr ""
-"程å¼é–‹ç™¼éšŽæ®µé¡¯ç¤ºå¾žç¬¬ä¸€æ¬¡æ›´å‹•è¨˜éŒ„ (commit) 到建立åˆä½µè«‹æ±‚ (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 "程å¼é–‹ç™¼éšŽæ®µé¡¯ç¤ºå¾žç¬¬ä¸€æ¬¡æ›´å‹•è¨˜éŒ„ (commit) 到建立åˆä½µè«‹æ±‚ (merge request) 的時間。建立第一個åˆä½µè«‹æ±‚後,資料將自動填入。"
msgid "The collection of events added to the data gathered for that stage."
msgstr "該階段中的相關事件集åˆã€‚"
@@ -846,34 +968,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 +992,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 ""
+
+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 +1177,9 @@ msgstr "上傳檔案"
msgid "UploadLink|click to upload"
msgstr "點擊上傳"
+msgid "Use the following registration token during setup:"
+msgstr ""
+
msgid "Use your global notification setting"
msgstr "使用全域通知設定"
@@ -1112,22 +1208,16 @@ msgid "Withdraw Access Request"
msgstr "å–消權é™ç”³è«‹"
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
-msgstr "å³å°‡è¦åˆªé™¤ %{group_name}。被刪除的群組完全無法救回來喔ï¼çœŸçš„「100%確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
+msgstr "å³å°‡è¦åˆªé™¤ %{group_name}。被刪除的群組無法復原ï¼çœŸçš„「確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
-msgstr "å³å°‡è¦åˆªé™¤ %{project_name_with_namespace}。被刪除的專案完全無法救回來喔ï¼çœŸçš„「100%確定ã€è¦é€™éº¼åšå—Žï¼Ÿ"
+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) 上建立檔案"
@@ -1147,27 +1237,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"
@@ -1185,4 +1267,4 @@ msgstr "通知信"
msgid "parent"
msgid_plural "parents"
-msgstr[0] "上層"
+msgstr[0] "上層" \ No newline at end of file
diff --git a/package.json b/package.json
index 1725658729a..feae6ca9748 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"babel-preset-latest": "^6.24.0",
"babel-preset-stage-2": "^6.22.0",
"bootstrap-sass": "^3.3.6",
+ "brace-expansion": "^1.1.8",
"compression-webpack-plugin": "^1.0.0",
"copy-webpack-plugin": "^4.0.1",
"core-js": "^2.4.1",
@@ -63,6 +64,7 @@
"vue-loader": "^11.3.4",
"vue-resource": "^1.3.4",
"vue-template-compiler": "^2.2.6",
+ "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 4db72be6f8c..08f328da542 100644
--- a/public/404.html
+++ b/public/404.html
@@ -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/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/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 3d21b695af4..aadd3317875 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -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 2fbab1e4040..be27bbb4283 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -241,13 +241,10 @@ describe AutocompleteController do
it 'returns projects' do
expect(json_response).to be_kind_of(Array)
- expect(json_response.size).to eq(2)
-
- expect(json_response.first['id']).to eq(0)
- expect(json_response.first['name_with_namespace']).to eq 'No project'
+ expect(json_response.size).to eq(1)
- expect(json_response.last['id']).to eq authorized_project.id
- expect(json_response.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
@@ -265,10 +262,10 @@ describe AutocompleteController do
it 'returns projects' do
expect(json_response).to be_kind_of(Array)
- expect(json_response.size).to eq(2)
+ expect(json_response.size).to eq(1)
- expect(json_response.last['id']).to eq authorized_search_project.id
- expect(json_response.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
@@ -292,7 +289,7 @@ describe AutocompleteController do
it 'returns projects' do
expect(json_response).to be_kind_of(Array)
- expect(json_response.size).to eq 3 # Of a total of 4
+ expect(json_response.size).to eq 2 # Of a total of 3
end
end
end
@@ -312,9 +309,9 @@ describe AutocompleteController do
get(:projects, project_id: project.id, offset_id: authorized_project.id)
end
- it 'returns "No project"' do
- expect(json_response.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there
- expect(json_response.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
@@ -331,12 +328,49 @@ describe AutocompleteController do
get(:projects, project_id: project.id)
end
- it 'returns a single "No project"' do
+ it 'returns no projects' do
expect(json_response).to be_kind_of(Array)
- expect(json_response.size).to eq(1) # 'No project'
- expect(json_response.first['id']).to eq 0
+ 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') }
+
+ context 'unauthorized user' do
+ it 'returns empty json' do
+ get :award_emojis
+
+ 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
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/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/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index b571b11dcac..5d9403c23ac 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -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
@@ -879,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/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/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_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index c0e48046937..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
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 7c5d059760f..be273acb69b 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -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/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 5bba1dec7db..c2b59239af9 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|
@@ -226,5 +227,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/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/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/projects.rb b/spec/factories/projects.rb
index 9ebda0ba03b..7493b0a8b35 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -101,8 +101,6 @@ FactoryGirl.define do
# Test repository - https://gitlab.com/gitlab-org/gitlab-test
trait :repository do
- path { 'gitlabhq' }
-
test_repo
transient do
diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb
index 07430ecd6e0..5ff791fc36a 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: 1)
+ expect(page.find('.sidebar-sub-level-items li.active')).to have_content(title)
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_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index dbb0ae9c86e..563818e8761 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -79,6 +79,22 @@ feature 'Admin updates settings' do
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/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/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/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/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb
index facb67ae787..ebc3d196118 100644
--- a/spec/features/dashboard/issues_filter_spec.rb
+++ b/spec/features/dashboard/issues_filter_spec.rb
@@ -50,7 +50,7 @@ feature 'Dashboard Issues filtering', :js do
it 'updates atom feed link' do
visit_issues(milestone_title: '', assignee_id: user.id)
- link = find('.nav-controls a[title="Subscribe"]')
+ link = find('.breadcrumbs a[title="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)
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/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 d0316cfb18d..b83bad3befb 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -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/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 9ba9f5686f7..2577d98df6f 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -25,7 +25,7 @@ feature 'Group merge requests page' do
end
it 'ignores archived merge request count badges in navbar' do
- expect( page.find('[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
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 20f9818b08b..4ec2e7e6012 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -158,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/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/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 2cc027aac9e..1c4649d0ba9 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -204,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 975dc035f2d..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
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 e84b07ec2ef..c46803112a9 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -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 5f99921ae2e..f6c2e952bea 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -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 a64c1cf220b..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])
+
+ create(:labeled_issue, title: "Issue with multiple words label", project: project, labels: [multiple_words_label])
- multiple_words_label_issue = create(:issue, title: "Issue with multiple words label", project: project)
- multiple_words_label_issue.labels << multiple_words_label
+ sign_in(user)
+ visit project_issues_path(project)
+ end
- future_milestone = create(:milestone, title: "future", project: project, due_date: Time.now + 1.month)
+ it 'filters by all available tokens' do
+ search_term = 'issue'
- create(:issue,
- title: "Issue with future milestone",
- milestone: future_milestone,
- project: project)
+ input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
- visit project_issues_path(project)
+ 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
@@ -104,59 +101,6 @@ describe 'Filter issues', js: true do
expect_filtered_search_input_empty
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}")
-
- wait_for_requests
-
- expect_tokens([author_token(user.name)])
- 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}")
-
- wait_for_requests
-
- expect_tokens([author_token(user.name), assignee_token(user.name)])
- 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}")
-
- wait_for_requests
-
- expect_tokens([
- author_token(user.name),
- assignee_token(user.name),
- label_token(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}")
-
- wait_for_requests
-
- expect_tokens([
- author_token(user.name),
- assignee_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
- end
end
describe 'filter issues by assignee' do
@@ -175,66 +119,13 @@ describe 'Filter issues', js: true do
input_filtered_search('assignee:none')
expect_tokens([assignee_token('none')])
- expect_issues_list_count(8, 1)
+ expect_issues_list_count(3)
expect_filtered_search_input_empty
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}")
-
- wait_for_requests
-
- expect_tokens([assignee_token(user.name)])
- 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}")
-
- wait_for_requests
-
- expect_tokens([assignee_token(user.name), author_token(user.name)])
- 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}")
-
- wait_for_requests
-
- expect_tokens([
- assignee_token(user.name),
- author_token(user.name),
- label_token(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([
- 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
- 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}")
@@ -248,7 +139,7 @@ describe 'Filter issues', js: true do
input_filtered_search('label:none')
expect_tokens([label_token('none', false)])
- expect_issues_list_count(9, 1)
+ expect_issues_list_count(8)
expect_filtered_search_input_empty
end
@@ -275,13 +166,13 @@ describe 'Filter issues', js: true do
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([label_token(new_label.title)])
- expect_no_issues_list()
+ expect_no_issues_list
expect_filtered_search_input_empty
end
end
@@ -344,95 +235,10 @@ describe 'Filter issues', js: true do
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([label_token(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}")
-
- wait_for_requests
-
- expect_tokens([label_token(caps_sensitive_label.title), author_token(user.name)])
- 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}")
-
- wait_for_requests
-
- expect_tokens([
- label_token(caps_sensitive_label.title),
- author_token(user.name),
- assignee_token(user.name)
- ])
- 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([
- 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)
- 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([
- label_token(bug_label.title),
- label_token(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}")
-
- wait_for_requests
-
- expect_tokens([
- label_token(bug_label.title),
- label_token(caps_sensitive_label.title),
- author_token(user.name)
- ])
- 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}")
-
- wait_for_requests
-
- expect_tokens([
- label_token(bug_label.title),
- label_token(caps_sensitive_label.title),
- author_token(user.name),
- assignee_token(user.name)
- ])
- 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
@@ -450,15 +256,10 @@ 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([label_token("\"#{multiple_words_label.title}\"")])
expect_filtered_search_input_empty
end
@@ -479,11 +280,15 @@ describe 'Filter issues', js: true do
input_filtered_search("milestone:none")
expect_tokens([milestone_token('none', false)])
- expect_issues_list_count(7, 1)
+ 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([milestone_token('upcoming', false)])
@@ -501,7 +306,7 @@ describe 'Filter issues', js: true do
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}")
@@ -510,70 +315,16 @@ describe 'Filter issues', js: true do
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([milestone_token(new_milestone.title)])
- expect_no_issues_list()
+ 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([milestone_token(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}")
-
- wait_for_requests
-
- expect_tokens([
- milestone_token(milestone.title),
- author_token(user.name)
- ])
- 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}")
-
- wait_for_requests
-
- expect_tokens([
- milestone_token(milestone.title),
- author_token(user.name),
- assignee_token(user.name)
- ])
- 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}")
-
- wait_for_requests
-
- expect_tokens([
- milestone_token(milestone.title),
- author_token(user.name),
- assignee_token(user.name),
- label_token(bug_label.title)
- ])
- expect_issues_list_count(2)
- expect_filtered_search_input(search_term)
- end
- end
end
describe 'filter issues by text' do
@@ -582,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
@@ -603,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')
@@ -746,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')
@@ -754,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
@@ -781,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
@@ -821,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
@@ -829,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 a432d031337..d4dd570fb37 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -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 14a555fde10..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
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 62dbc3efb01..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
+ 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)
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
- 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,16 +94,15 @@ 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)
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 3ffc80622f5..11db1105d91 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 '.breadcrumbs' do
+ click_link 'New issue'
+ end
expect(current_path).to eq new_user_session_path
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 c4f02311f13..e77f1f92731 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
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
index a8f5dc275e4..e9068f722d5 100644
--- a/spec/features/merge_requests/diffs_spec.rb
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -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/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/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb
index f89dd38e5cd..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
@@ -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"]'))
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/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb
index dcd0449dbcb..171e061e60e 100644
--- a/spec/features/profiles/account_spec.rb
+++ b/spec/features/profiles/account_spec.rb
@@ -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/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/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 2eb6fab129d..ad2db1a34f4 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -18,23 +18,25 @@ feature 'Import/Export - project import integration test', js: true do
context 'when selecting the namespace' do
let(:user) { create(:admin) }
- let!(:namespace) { create(:namespace, name: "asd", owner: user) }
+ let!(:namespace) { create(:namespace, name: 'asd', owner: user) }
+ let(:project_path) { 'test-project-path' + SecureRandom.hex }
context 'prefilled the path' do
scenario 'user imports an exported project successfully' do
visit new_project_path
select2(namespace.id, from: '#project_namespace_id')
- fill_in :project_path, with: 'test-project-path', visible: true
+ fill_in :project_path, with: 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=test-project-path")
- expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\z/).and_call_original
+ 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
attach_file('file', file)
+ click_on 'Import project'
- expect { click_on 'Import project' }.to change { Project.count }.by(1)
+ expect(Project.count).to eq(1)
project = Project.last
expect(project).not_to be_nil
@@ -64,7 +66,7 @@ feature 'Import/Export - project import integration test', js: true do
end
scenario 'invalid project' do
- namespace = create(:namespace, name: "asd", owner: user)
+ namespace = create(:namespace, name: 'asdf', owner: user)
project = create(:project, namespace: namespace)
visit new_project_path
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/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 80d91e5915f..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
@@ -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
@@ -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/sub_group_issuables_spec.rb b/spec/features/projects/sub_group_issuables_spec.rb
index aaf64d42515..b2b39dbd24c 100644
--- a/spec/features/projects/sub_group_issuables_spec.rb
+++ b/spec/features/projects/sub_group_issuables_spec.rb
@@ -24,7 +24,7 @@ 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'
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 baf3d29e6c5..81f7ab80a04 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -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/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 785cfeb34bd..c7f0e342809 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -43,6 +43,21 @@ feature 'Runners' do
expect(page).not_to have_content(specific_runner.display_name)
end
+ scenario 'user edits the runner to be protected' do
+ visit runners_path(project)
+
+ within '.activated-specific-runners' do
+ first('.edit-runner > a').click
+ end
+
+ expect(page.find_field('runner[access_level]')).not_to be_checked
+
+ check 'runner_access_level'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Protected Yes'
+ end
+
context 'when a runner has a tag' do
background do
specific_runner.update(tag_list: ['tag'])
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 31d509455ba..05a089641f1 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
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/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index 580258f77eb..aeb0534b733 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -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/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/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/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/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/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/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/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/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/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
index 5eba03ef576..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
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/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 8c68ceff914..2aa4fb1f6c6 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -101,12 +101,13 @@ describe('Api', () => {
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);
@@ -124,10 +125,11 @@ describe('Api', () => {
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?simple=true`;
+ 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/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/blob.rb b/spec/javascripts/fixtures/blob.rb
index 2dffc42b0ef..81e8a51a902 100644
--- a/spec/javascripts/fixtures/blob.rb
+++ b/spec/javascripts/fixtures/blob.rb
@@ -17,6 +17,10 @@ describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
sign_in(admin)
end
+ after do
+ remove_repository(project)
+ end
+
it 'blob/show.html.raw' do |example|
get(:show,
namespace_id: project.namespace,
diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb
index bb3bdf7c215..4fc072d2585 100644
--- a/spec/javascripts/fixtures/branches.rb
+++ b/spec/javascripts/fixtures/branches.rb
@@ -17,6 +17,10 @@ describe Projects::BranchesController, '(JavaScript fixtures)', type: :controlle
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 793ffa7c220..7fa351680c9 100644
--- a/spec/javascripts/fixtures/dashboard.rb
+++ b/spec/javascripts/fixtures/dashboard.rb
@@ -17,6 +17,10 @@ describe Dashboard::ProjectsController, '(JavaScript fixtures)', type: :controll
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 bea161c514f..580894ceaf9 100644
--- a/spec/javascripts/fixtures/deploy_keys.rb
+++ b/spec/javascripts/fixtures/deploy_keys.rb
@@ -16,6 +16,10 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control
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/issues.rb b/spec/javascripts/fixtures/issues.rb
index d3ad50af1b9..0ee2f82dfd6 100644
--- a/spec/javascripts/fixtures/issues.rb
+++ b/spec/javascripts/fixtures/issues.rb
@@ -17,6 +17,10 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
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 83a96797506..87d131dfe28 100644
--- a/spec/javascripts/fixtures/jobs.rb
+++ b/spec/javascripts/fixtures/jobs.rb
@@ -21,6 +21,10 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller 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 814f065f3a4..b730d557e21 100644
--- a/spec/javascripts/fixtures/labels.rb
+++ b/spec/javascripts/fixtures/labels.rb
@@ -19,6 +19,10 @@ 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
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index f97a5d2b5de..4bc2205e642 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -37,6 +37,10 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
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 6e0a97d2e3f..ddce00bc0fe 100644
--- a/spec/javascripts/fixtures/merge_requests_diffs.rb
+++ b/spec/javascripts/fixtures/merge_requests_diffs.rb
@@ -29,6 +29,10 @@ describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type
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/projects.rb b/spec/javascripts/fixtures/projects.rb
index f09d44a49d1..2a100e7fab5 100644
--- a/spec/javascripts/fixtures/projects.rb
+++ b/spec/javascripts/fixtures/projects.rb
@@ -17,6 +17,10 @@ describe ProjectsController, '(JavaScript fixtures)', type: :controller 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 7968c9425f2..f95f8038ffb 100644
--- a/spec/javascripts/fixtures/prometheus_service.rb
+++ b/spec/javascripts/fixtures/prometheus_service.rb
@@ -18,6 +18,10 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle
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 80915c32a74..9280ed5a7f1 100644
--- a/spec/javascripts/fixtures/services.rb
+++ b/spec/javascripts/fixtures/services.rb
@@ -18,6 +18,10 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle
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 01bfb87b0c1..fa97f352e31 100644
--- a/spec/javascripts/fixtures/snippet.rb
+++ b/spec/javascripts/fixtures/snippet.rb
@@ -18,6 +18,10 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller 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 ba630365c18..426b854fe8b 100644
--- a/spec/javascripts/fixtures/todos.rb
+++ b/spec/javascripts/fixtures/todos.rb
@@ -15,6 +15,10 @@ 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
diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js
index 2e81a1b056b..4588bf3d971 100644
--- a/spec/javascripts/fly_out_nav_spec.js
+++ b/spec/javascripts/fly_out_nav_spec.js
@@ -1,4 +1,3 @@
-import Cookies from 'js-cookie';
import {
calculateTop,
showSubLevelItems,
@@ -6,11 +5,14 @@ import {
canShowActiveSubItems,
mouseEnterTopItems,
mouseLeaveTopItem,
+ getOpenMenu,
setOpenMenu,
mousePos,
getHideSubItemsInterval,
documentMouseMove,
getHeaderHeight,
+ setSidebar,
+ subItemsMouseLeave,
} from '~/fly_out_nav';
import bp from '~/breakpoints';
@@ -113,7 +115,6 @@ describe('Fly out sidebar navigation', () => {
clientX: el.getBoundingClientRect().left + 20,
clientY: el.getBoundingClientRect().top + 10,
});
- console.log(el);
expect(
getHideSubItemsInterval(),
@@ -283,7 +284,7 @@ describe('Fly out sidebar navigation', () => {
describe('canShowActiveSubItems', () => {
afterEach(() => {
- Cookies.remove('sidebar_collapsed');
+ setSidebar(null);
});
it('returns true by default', () => {
@@ -292,40 +293,52 @@ describe('Fly out sidebar navigation', () => {
).toBeTruthy();
});
- it('returns false when cookie is false & element is active', () => {
- Cookies.set('sidebar_collapsed', 'false');
+ 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 cookie is false & element is active', () => {
- Cookies.set('sidebar_collapsed', 'true');
+ 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();
});
+ });
- it('returns true when element is active & breakpoint is sm', () => {
- breakpointSize = 'sm';
- el.classList.add('active');
+ 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(
- canShowActiveSubItems(el),
- ).toBeTruthy();
+ getOpenMenu(),
+ ).toBeNull();
});
- it('returns true when element is active & breakpoint is md', () => {
- breakpointSize = 'md';
- el.classList.add('active');
+ it('does not hide subMenu if element is hovered', () => {
+ el.classList.add('is-over');
+ subItemsMouseLeave(el);
expect(
- canShowActiveSubItems(el),
- ).toBeTruthy();
+ getOpenMenu(),
+ ).not.toBeNull();
});
});
});
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 10fcc590c89..dcb8dbce178 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -4,7 +4,10 @@ import '~/gl_dropdown';
import '~/lib/utils/common_utils';
import '~/lib/utils/url_utility';
-(() => {
+describe('glDropdown', function describeDropdown() {
+ preloadFixtures('static/gl_dropdown.html.raw');
+ loadJSONFixtures('projects.json');
+
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
@@ -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/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/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/monitoring_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index 6c7b691baa4..752fdfb4614 100644
--- a/spec/javascripts/monitoring/monitoring_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -1,21 +1,21 @@
import Vue from 'vue';
-import Monitoring from '~/monitoring/components/monitoring.vue';
+import Dashboard from '~/monitoring/components/dashboard.vue';
import { MonitorMockInterceptor } from './mock_data';
-describe('Monitoring', () => {
+describe('Dashboard', () => {
const fixtureName = 'environments/metrics/metrics.html.raw';
- let MonitoringComponent;
+ let DashboardComponent;
let component;
preloadFixtures(fixtureName);
beforeEach(() => {
loadFixtures(fixtureName);
- MonitoringComponent = Vue.extend(Monitoring);
+ DashboardComponent = Vue.extend(Dashboard);
});
describe('no metrics are available yet', () => {
it('shows a getting started empty state when no metrics are present', () => {
- component = new MonitoringComponent({
+ component = new DashboardComponent({
el: document.querySelector('#prometheus-graphs'),
});
@@ -36,7 +36,7 @@ describe('Monitoring', () => {
});
it('shows up a loading state', (done) => {
- component = new MonitoringComponent({
+ component = new DashboardComponent({
el: document.querySelector('#prometheus-graphs'),
});
component.$mount();
diff --git a/spec/javascripts/monitoring/monitoring_state_spec.js b/spec/javascripts/monitoring/dashboard_state_spec.js
index 4c0c558502f..e8f7042e131 100644
--- a/spec/javascripts/monitoring/monitoring_state_spec.js
+++ b/spec/javascripts/monitoring/dashboard_state_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
-import MonitoringState from '~/monitoring/components/monitoring_state.vue';
+import EmptyState from '~/monitoring/components/empty_state.vue';
import { statePaths } from './mock_data';
const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringState);
+ const Component = Vue.extend(EmptyState);
return new Component({
propsData,
@@ -14,7 +14,7 @@ function getTextFromNode(component, selector) {
return component.$el.querySelector(selector).firstChild.nodeValue.trim();
}
-describe('MonitoringState', () => {
+describe('EmptyState', () => {
describe('Computed props', () => {
it('currentState', () => {
const component = createComponent({
diff --git a/spec/javascripts/monitoring/monitoring_deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js
index 5cc5b514824..c2ff38ffab9 100644
--- a/spec/javascripts/monitoring/monitoring_deployment_spec.js
+++ b/spec/javascripts/monitoring/graph/deployment_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
-import MonitoringState from '~/monitoring/components/monitoring_deployment.vue';
-import { deploymentData } from './mock_data';
+import GraphDeployment from '~/monitoring/components/graph/deployment.vue';
+import { deploymentData } from '../mock_data';
const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringState);
+ const Component = Vue.extend(GraphDeployment);
return new Component({
propsData,
diff --git a/spec/javascripts/monitoring/monitoring_flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js
index 3861a95ff07..14794cbfd50 100644
--- a/spec/javascripts/monitoring/monitoring_flag_spec.js
+++ b/spec/javascripts/monitoring/graph/flag_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
-import MonitoringFlag from '~/monitoring/components/monitoring_flag.vue';
+import GraphFlag from '~/monitoring/components/graph/flag.vue';
const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringFlag);
+ const Component = Vue.extend(GraphFlag);
return new Component({
propsData,
@@ -14,7 +14,7 @@ function getCoordinate(component, selector, coordinate) {
return parseInt(coordinateVal, 10);
}
-describe('MonitoringFlag', () => {
+describe('GraphFlag', () => {
it('has a line and a circle located at the currentXCoordinate and currentYCoordinate', () => {
const component = createComponent({
currentXCoordinate: 200,
@@ -32,10 +32,6 @@ describe('MonitoringFlag', () => {
.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', () => {
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/monitoring_row_spec.js b/spec/javascripts/monitoring/graph_row_spec.js
index a82480e8342..6a79d7c8f82 100644
--- a/spec/javascripts/monitoring/monitoring_row_spec.js
+++ b/spec/javascripts/monitoring/graph_row_spec.js
@@ -1,20 +1,25 @@
import Vue from 'vue';
-import MonitoringRow from '~/monitoring/components/monitoring_row.vue';
-import { deploymentData, singleRowMetrics } from './mock_data';
+import GraphRow from '~/monitoring/components/graph_row.vue';
+import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
+import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringRow);
+ const Component = Vue.extend(GraphRow);
return new Component({
propsData,
}).$mount();
};
-describe('MonitoringRow', () => {
+const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
+describe('GraphRow', () => {
+ beforeEach(() => {
+ spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({});
+ });
describe('Computed props', () => {
it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => {
const component = createComponent({
- rowData: singleRowMetrics,
+ rowData: convertedMetrics,
updateAspectRatio: false,
deploymentData,
});
@@ -24,7 +29,7 @@ describe('MonitoringRow', () => {
it('bootstrapClass is set to col-md-12 when rowData is lower than 2', () => {
const component = createComponent({
- rowData: [singleRowMetrics[0]],
+ rowData: [convertedMetrics[0]],
updateAspectRatio: false,
deploymentData,
});
@@ -35,7 +40,7 @@ describe('MonitoringRow', () => {
it('has one column', () => {
const component = createComponent({
- rowData: singleRowMetrics,
+ rowData: convertedMetrics,
updateAspectRatio: false,
deploymentData,
});
@@ -46,7 +51,7 @@ describe('MonitoringRow', () => {
it('has two columns', () => {
const component = createComponent({
- rowData: singleRowMetrics,
+ rowData: convertedMetrics,
updateAspectRatio: false,
deploymentData,
});
diff --git a/spec/javascripts/monitoring/monitoring_column_spec.js b/spec/javascripts/monitoring/graph_spec.js
index c423024dce0..7d8b0744af1 100644
--- a/spec/javascripts/monitoring/monitoring_column_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -1,59 +1,39 @@
import Vue from 'vue';
-import _ from 'underscore';
-import MonitoringColumn from '~/monitoring/components/monitoring_column.vue';
+import Graph from '~/monitoring/components/graph.vue';
import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
import eventHub from '~/monitoring/event_hub';
-import { deploymentData, singleRowMetrics } from './mock_data';
+import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringColumn);
+ const Component = Vue.extend(Graph);
return new Component({
propsData,
}).$mount();
};
-describe('MonitoringColumn', () => {
+const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
+
+describe('Graph', () => {
beforeEach(() => {
- spyOn(MonitoringMixins.methods, 'formatDeployments').and.callFake(function fakeFormat() {
- return {};
- });
+ spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({});
});
it('has a title', () => {
const component = createComponent({
- columnData: singleRowMetrics[0],
+ graphData: convertedMetrics[1],
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();
- });
+ 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({
- columnData: singleRowMetrics[0],
+ graphData: convertedMetrics[1],
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
@@ -66,7 +46,7 @@ describe('MonitoringColumn', () => {
it('outterViewBox gets a width and height property based on the DOM size of the element', () => {
const component = createComponent({
- columnData: singleRowMetrics[0],
+ graphData: convertedMetrics[1],
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
@@ -81,7 +61,7 @@ describe('MonitoringColumn', () => {
it('sends an event to the eventhub when it has finished resizing', (done) => {
const component = createComponent({
- columnData: singleRowMetrics[0],
+ graphData: convertedMetrics[1],
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
@@ -97,13 +77,13 @@ describe('MonitoringColumn', () => {
it('has a title for the y-axis and the chart legend that comes from the backend', () => {
const component = createComponent({
- columnData: singleRowMetrics[0],
+ graphData: convertedMetrics[1],
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
});
- expect(component.yAxisLabel).toEqual(component.columnData.y_label);
- expect(component.legendTitle).toEqual(component.columnData.queries[0].label);
+ 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_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/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/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/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 021804e0769..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');
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
deleted file mode 100644
index 3d36bb3e4d4..00000000000
--- a/spec/javascripts/project_title_spec.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/* global Project */
-
-import 'select2/select2';
-import '~/gl_dropdown';
-import '~/api';
-import '~/project_select';
-import '~/project';
-
-describe('Project Title', () => {
- const dummyApiVersion = 'v3000';
- preloadFixtures('issues/open-issue.html.raw');
- loadJSONFixtures('projects.json');
-
- beforeEach(() => {
- loadFixtures('issues/open-issue.html.raw');
-
- window.gon = {};
- window.gon.api_version = dummyApiVersion;
-
- // 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');
- window.gon.current_user_id = 1;
- $('.js-projects-dropdown-toggle').click();
- expect($menu).toHaveClass('open');
- expect(reqUrl).toBe(`/api/${dummyApiVersion}/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..59fc2dedba5
--- /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('No projects matched your query');
+ });
+ });
+ });
+
+ 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..f2a23e33325
--- /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 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/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/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/groups/group_identicon_spec.js b/spec/javascripts/vue_shared/components/identicon_spec.js
index 66772327503..647680f00f7 100644
--- a/spec/javascripts/groups/group_identicon_spec.js
+++ b/spec/javascripts/vue_shared/components/identicon_spec.js
@@ -1,29 +1,30 @@
import Vue from 'vue';
-import groupIdenticonComponent from '~/groups/components/group_identicon.vue';
-import GroupsStore from '~/groups/stores/groups_store';
-import { group1 } from './mock_data';
+import identiconComponent from '~/vue_shared/components/identicon.vue';
-const createComponent = () => {
- const Component = Vue.extend(groupIdenticonComponent);
- const store = new GroupsStore();
- const group = store.decorateGroup(group1);
+const createComponent = (sizeClass) => {
+ const Component = Vue.extend(identiconComponent);
return new Component({
propsData: {
- entityId: group.id,
- entityName: group.name,
+ entityId: 1,
+ entityName: 'entity-name',
+ sizeClass,
},
}).$mount();
};
-describe('GroupIdenticonComponent', () => {
- let vm;
+describe('IdenticonComponent', () => {
+ describe('computed', () => {
+ let vm;
- beforeEach(() => {
- vm = createComponent();
- });
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
- describe('computed', () => {
describe('identiconStyles', () => {
it('should return styles attribute value with `background-color` property', () => {
vm.entityId = 4;
@@ -52,9 +53,20 @@ describe('GroupIdenticonComponent', () => {
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/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index ee28387cd48..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
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/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/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/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 c25fd459dd7..1bcdc369c44 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -450,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')
@@ -477,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')
@@ -506,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}/)
@@ -517,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}/)
@@ -573,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
@@ -582,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
@@ -890,4 +898,20 @@ describe Gitlab::Database::MigrationHelpers do
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/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/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 6b9773c9b63..08959e7bc16 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -433,6 +433,40 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#delete_refs' do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
+ end
+
+ it 'deletes the ref' do
+ @repo.delete_refs('refs/heads/feature')
+
+ expect(@repo.rugged.references['refs/heads/feature']).to be_nil
+ end
+
+ 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
+
+ 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
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+ end
+
describe "#refs_hash" do
let(:refs) { repository.refs_hash }
@@ -882,27 +916,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)
+ end
+ end
- expect(branch).to eq(nil)
+ context 'when Gitaly find_branch feature is enabled' do
+ it_behaves_like 'finding a branch'
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 disabled', skip_gitaly_mock: true do
+ it_behaves_like 'finding a branch'
+
+ it 'should reload Rugged::Repository and return master' do
+ expect(Rugged::Repository).to receive(:new).twice.and_call_original
- repository.find_branch('master')
- branch = repository.find_branch('master', force_reload: true)
+ 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')
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 295a979da76..458627ee4de 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -155,6 +155,44 @@ describe Gitlab::GitAccess do
end
end
+ shared_examples '#check with a key that is not valid' do
+ before do
+ project.add_master(user)
+ end
+
+ context 'key is too small' do
+ before do
+ stub_application_setting(rsa_key_restriction: 4096)
+ 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 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)
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index e521fcc6dc1..b07462e4978 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -2,45 +2,9 @@ require 'rails_helper'
describe Gitlab::Gpg::Commit do
describe '#signature' do
- let!(:project) { create :project, :repository, path: 'sample-project' }
- let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
-
- context 'unsigned commit' do
- it 'returns nil' do
- expect(described_class.new(project, commit_sha).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
-
- 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(project, commit_sha).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(project, commit_sha)
+ gpg_commit = described_class.new(commit)
expect(gpg_commit).to receive(:using_keychain).and_call_original
gpg_commit.signature
@@ -51,11 +15,140 @@ 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' }
- before do
- allow(Rugged::Commit).to receive(:extract_signature)
+ context 'unsigned commit' do
+ let!(:commit) { create :commit, project: project, sha: commit_sha }
+
+ it 'returns nil' do
+ expect(described_class.new(commit).signature).to be_nil
+ end
+ end
+
+ 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
+
+ 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(
[
@@ -63,33 +156,27 @@ describe Gitlab::Gpg::Commit do
GpgHelpers::User1.signed_commit_base_data
]
)
- end
-
- it 'returns an invalid signature' do
- expect(described_class.new(project, commit_sha).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
- )
- end
-
- it 'returns the cached signature on second call' do
- gpg_commit = described_class.new(project, commit_sha)
-
- expect(gpg_commit).to receive(:using_keychain).and_call_original
- gpg_commit.signature
+ 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
- # consecutive call
- expect(gpg_commit).not_to receive(:using_keychain).and_call_original
- gpg_commit.signature
+ it_behaves_like 'returns the cached signature on second call'
end
end
- context 'unknown public key' do
+ 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)
@@ -102,27 +189,18 @@ describe Gitlab::Gpg::Commit do
end
it 'returns an invalid signature' do
- expect(described_class.new(project, commit_sha).signature).to have_attributes(
+ expect(described_class.new(commit).signature).to have_attributes(
commit_sha: commit_sha,
project: project,
gpg_key: nil,
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(project, commit_sha)
-
- 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 4de4419de27..b9fd4d02156 100644
--- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
+++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
@@ -4,8 +4,29 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
describe '#run' 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,
+ committer_email: GpgHelpers::User1.emails.first
+ )
+
+ allow(raw_commit).to receive :save!
+
+ raw_commit
+ end
+
+ let!(:commit) do
+ create :commit, git_commit: raw_commit, project: project
+ end
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(
@@ -25,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
@@ -39,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
@@ -54,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
@@ -68,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
@@ -82,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
@@ -97,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
@@ -115,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
@@ -123,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)
@@ -133,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
@@ -147,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
@@ -158,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 30ad033b204..11a2aea1915 100644
--- a/spec/lib/gitlab/gpg_spec.rb
+++ b/spec/lib/gitlab/gpg_spec.rb
@@ -42,6 +42,21 @@ 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
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/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index a5e03e149a7..b852ac570a3 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -224,6 +224,7 @@ Ci::Pipeline:
- lock_version
- auto_canceled_by_id
- pipeline_schedule_id
+- protected
Ci::Stage:
- id
- name
@@ -276,6 +277,8 @@ CommitStatus:
- coverage_regex
- auto_canceled_by_id
- retried
+- protected
+- failure_reason
Ci::Variable:
- id
- project_id
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/key_fingerprint_spec.rb b/spec/lib/gitlab/key_fingerprint_spec.rb
deleted file mode 100644
index d643dc5342d..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/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/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 cfadee0bcf5..c7930378240 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -186,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 = {})
@@ -216,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')
@@ -237,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: '')
@@ -245,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')
@@ -253,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')
@@ -261,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: '')
@@ -269,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')
@@ -278,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/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/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/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/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 92787bb262e..3137a72fdc4 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Utils do
- delegate :to_boolean, :boolean_to_yes_no, :slugify, to: :described_class
+ delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, to: :described_class
describe '.slugify' do
{
@@ -53,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/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index b66afafa174..699184ad9fe 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -228,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/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/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 1fa59ebd22b..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)
@@ -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,7 +643,6 @@ 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 do
@@ -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}") }
@@ -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/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 359753b600e..f921545668d 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -72,6 +72,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 +468,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/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 4f77f0d85cd..08d22f166e4 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -43,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
@@ -1257,8 +1283,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
@@ -1462,10 +1492,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
@@ -1474,8 +1506,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
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index b84e3ff18e8..84656ffe0b9 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -546,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) }
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/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 c18c635d811..11e64a0f877 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -195,6 +195,67 @@ eos
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 f7583645e69..858ec831200 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -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 dfbe1a7c192..37f6fd3a25b 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
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/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..f9cd12c0ff3 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -84,6 +84,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 de86788d142..e547da0cfbe 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -769,4 +769,22 @@ describe Issue do
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/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 2d10c6ef1da..f5d079c27c4 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
@@ -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) }
@@ -1370,29 +1389,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
@@ -1701,4 +1702,16 @@ describe MergeRequest do
.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/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index b1743cd608e..537cdadd528 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -203,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 2e613c44357..be1ae295f75 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -181,7 +181,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
@@ -1563,10 +1563,18 @@ describe Project do
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
@@ -1581,6 +1589,7 @@ describe Project do
project.import_finish
+ expect(after_import_service).to have_received(:execute)
expect(housekeeping_service).to have_received(:execute)
end
@@ -2225,6 +2234,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) }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 462e92b8b62..7065d467ec0 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -886,7 +886,7 @@ describe Repository, models: true do
context 'when pre hooks were successful' do
it 'runs without errors' do
expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
- .with(committer, repository, old_rev, blank_sha, 'refs/heads/feature')
+ .with(committer, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature')
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
end
@@ -923,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 = Gitlab::Git::HooksService.new
expect(Gitlab::Git::HooksService).to receive(:new).and_return(service)
expect(service).to receive(:execute)
- .with(committer, repository, 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(committer, 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(committer, repository)
+ service = Gitlab::Git::OperationService.new(committer, repository.raw_repository)
expect(service).to receive(:update_autocrlf_option)
@@ -953,20 +956,51 @@ describe Repository, models: true do
it 'updates the head' do
expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev)
- GitOperationService.new(committer, 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).to receive(:run_git)
+ expect(target_project.repository.raw_repository).to receive(:run_git)
end
it 'raises Rugged::ReferenceError' do
@@ -975,9 +1009,9 @@ describe Repository, models: true do
end
expect do
- GitOperationService.new(committer, target_project.repository)
+ Gitlab::Git::OperationService.new(committer, target_project.repository.raw_repository)
.with_branch('feature',
- start_project: project,
+ start_repository: project.repository.raw_repository,
&:itself)
end.to raise_reference_error
end
@@ -997,7 +1031,7 @@ describe Repository, models: true do
repository.add_branch(user, branch, old_rev)
expect do
- GitOperationService.new(committer, 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
@@ -1015,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(committer, 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
@@ -1027,7 +1061,7 @@ describe Repository, models: true do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
- GitOperationService.new(committer, repository).with_branch('feature') do
+ Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do
new_rev
end
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
@@ -1045,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(committer, repository)
- .with_branch('new-feature') do
- new_rev
- end
+ repository.with_branch(user, 'new-feature') do
+ new_rev
+ end
end
end
@@ -1105,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) }
@@ -1113,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
@@ -1127,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
@@ -1142,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
@@ -1345,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
@@ -1725,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
@@ -2036,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
@@ -2063,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/user_spec.rb b/spec/models/user_spec.rb
index 8e04eea56a7..fd83a58ed9f 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -789,6 +789,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
@@ -802,6 +803,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
@@ -830,6 +839,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
@@ -2085,4 +2102,18 @@ describe User do
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
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/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 3c02e6302b4..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'] } }
@@ -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 dafe3f466a2..edbfaf510c5 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -565,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)
@@ -804,7 +804,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 e497ec333a2..684877c33c0 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -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/files_spec.rb b/spec/requests/api/files_spec.rb
index ea97c556430..114019441a3 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -125,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"
@@ -215,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
@@ -330,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 2179790d098..93b9cf85c1d 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -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 a7557c7fb22..77c43f92456 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -444,6 +444,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
@@ -488,6 +489,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)
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index e9c30dba8d4..a6c804fb2b3 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -660,6 +660,95 @@ describe API::Internal do
# end
# end
+ describe 'POST /internal/post_receive' do
+ let(:gl_repository) { "project-#{project.id}" }
+ let(:identifier) { 'key-123' }
+ let(:reference_counter) { double('ReferenceCounter') }
+
+ 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
+
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/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_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 0db645863fb..21d2c9644fb 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -117,6 +117,18 @@ describe API::MergeRequests do
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)
+ end
end
end
@@ -698,6 +710,10 @@ describe API::MergeRequests do
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
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 1fc0ec528b9..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)
@@ -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 a89a58ff713..4490e50702b 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1029,6 +1029,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
@@ -1104,23 +1108,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
@@ -1188,14 +1200,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) }
+
+ before do
+ create(:project_group_link, group: group, project: project)
+ end
+
+ it 'returns 204 when deleting a group share' do
+ delete api("/projects/#{project.id}/share/#{group.id}", user)
- 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
- expect(response).to have_http_status(204)
- expect(project.project_group_links).to be_empty
+ 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
@@ -1519,6 +1540,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]
@@ -1549,6 +1575,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 1aa8a95780e..07d7f96bd70 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -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)
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index e9ee3dd679d..12720355a6d 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
@@ -552,17 +557,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
@@ -621,13 +645,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
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index c8ff25f70fa..67907579225 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -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
@@ -279,6 +281,10 @@ describe API::Runners do
expect(response).to have_http_status(204)
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
context 'when runner is not shared' do
@@ -332,6 +338,10 @@ describe API::Runners do
expect(response).to have_http_status(204)
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
@@ -463,6 +473,10 @@ describe API::Runners do
expect(response).to have_http_status(204)
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
context 'when runner have one associated projects' do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 737c028ad53..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
@@ -44,7 +48,11 @@ describe API::Settings, 'Settings' do
help_page_text: 'custom help text',
help_page_hide_commercial_content: true,
help_page_support_url: 'http://example.com/help',
- project_export_enabled: false
+ 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)
@@ -61,6 +69,10 @@ describe API::Settings, 'Settings' do
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 d09b8bc42f1..d3905f698bd 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -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 1e206fd2a9e..922b99a6cba 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -84,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
@@ -293,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 49739a1601a..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') }
@@ -733,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
@@ -749,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
@@ -838,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
@@ -876,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)
@@ -1116,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)
@@ -1137,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
@@ -1239,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)
@@ -1551,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/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/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/project_snippets_spec.rb b/spec/requests/api/v3/project_snippets_spec.rb
index 3963924a066..7e88489082a 100644
--- a/spec/requests/api/v3/project_snippets_spec.rb
+++ b/spec/requests/api/v3/project_snippets_spec.rb
@@ -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/snippets_spec.rb b/spec/requests/api/v3/snippets_spec.rb
index 9ead3cad8bb..79860725634 100644
--- a/spec/requests/api/v3/snippets_spec.rb
+++ b/spec/requests/api/v3/snippets_spec.rb
@@ -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 d4648136841..7ccf387f2dc 100644
--- a/spec/requests/api/v3/triggers_spec.rb
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -37,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
@@ -80,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
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/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index fdd0cea4f3b..49d7c663128 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -391,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
@@ -413,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
@@ -431,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)
@@ -472,10 +471,11 @@ describe Ci::CreatePipelineService do
let(:user) {}
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
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 cec667071cc..f5ed9ff608f 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].freeze
+ user_id auto_canceled_by_id retried failure_reason].freeze
shared_examples 'build duplication' do
let(:stage) do
@@ -48,7 +48,7 @@ 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
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index e3c1bdce300..cc3d4e7da49 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -617,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
@@ -638,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
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 78b11cd7991..cc3d648c340 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -370,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
@@ -392,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/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index ccb770e01d0..15a50b85f19 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -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/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 5cfdb5372f3..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') }
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/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 088b7b4fc04..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
@@ -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/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 21c4b30734c..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)
@@ -73,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/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index 385f56e447f..9386c110385 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -23,6 +23,12 @@ describe Projects::HousekeepingService do
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 do
expect(subject).to receive(:try_obtain_lease).and_return(false)
@@ -39,6 +45,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/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/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 1b282e82187..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) }
@@ -132,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/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 e6a18654651..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 }
@@ -29,8 +29,7 @@ describe SystemNoteService do
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 }
@@ -185,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) { [] }
@@ -294,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)
@@ -312,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
@@ -390,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'}
@@ -408,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
@@ -424,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
@@ -471,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
@@ -491,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
@@ -533,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 }
@@ -561,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') }
@@ -899,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
@@ -1023,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
@@ -1078,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/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index 39586d37e93..934b4557ba2 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -80,7 +80,8 @@ module CycleAnalyticsHelpers
sha: project.repository.commit('master').sha,
ref: 'master',
source: :push,
- project: project)
+ project: project,
+ protected: false)
end
def new_dummy_job(environment)
@@ -93,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/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 5a0e7c3d099..192a2fed0a8 100644
--- a/spec/support/features/reportable_note_shared_examples.rb
+++ b/spec/support/features/reportable_note_shared_examples.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-shared_examples 'reportable note' do
+shared_examples 'reportable note' do |type|
include NotesHelper
let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") }
@@ -20,7 +20,12 @@ shared_examples 'reportable note' do
open_dropdown(dropdown)
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
diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb
index 99b8b6b7ea4..05021ea9054 100644
--- a/spec/support/filtered_search_helpers.rb
+++ b/spec/support/filtered_search_helpers.rb
@@ -58,11 +58,17 @@ module FilteredSearchHelpers
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
@@ -89,6 +95,10 @@ module FilteredSearchHelpers
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/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/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/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_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 1e39f80699c..290ded3ff7e 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',
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/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/projects/commits/_commit.html.haml_spec.rb b/spec/views/projects/commits/_commit.html.haml_spec.rb
index 4c247361bd7..00547e433c4 100644
--- a/spec/views/projects/commits/_commit.html.haml_spec.rb
+++ b/spec/views/projects/commits/_commit.html.haml_spec.rb
@@ -1,6 +1,10 @@
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 }
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index 1af422941d7..c1398629749 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -10,7 +10,9 @@ 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(view).to receive_messages(current_user: user,
+ can?: true,
+ current_application_settings: Gitlab::CurrentSettings.current_application_settings)
end
context 'LFS enabled setting' do
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/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/create_gpg_signature_worker_spec.rb b/spec/workers/create_gpg_signature_worker_spec.rb
index 54978baca88..aa6c347d738 100644
--- a/spec/workers/create_gpg_signature_worker_spec.rb
+++ b/spec/workers/create_gpg_signature_worker_spec.rb
@@ -7,9 +7,14 @@ describe CreateGpgSignatureWorker do
let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
it 'calls Gitlab::Gpg::Commit#signature' do
- expect(Gitlab::Gpg::Commit).to receive(:new).with(project, commit_sha).and_call_original
+ commit = instance_double(Commit)
+ gpg_commit = instance_double(Gitlab::Gpg::Commit)
- expect_any_instance_of(Gitlab::Gpg::Commit).to receive(:signature)
+ 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(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
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 05f971dfd13..c4979792194 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -23,8 +23,8 @@ describe GitGarbageCollectWorker do
expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
- expect_any_instance_of(Repository).to receive(:branch_count).and_call_original
- expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
+ expect_any_instance_of(Gitlab::Git::Repository).to receive(: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
@@ -143,7 +143,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/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/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/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/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/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/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 22fd88a55a3..89c66054885 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -151,7 +151,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/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/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/yarn.lock b/yarn.lock
index 396737a64a7..de4a9ac4487 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -990,7 +990,7 @@ brace-expansion@^1.0.0:
balanced-match "^0.4.1"
concat-map "0.0.1"
-brace-expansion@^1.1.7:
+brace-expansion@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
dependencies:
@@ -6307,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"